|
| 1 | +#!/usr/bin/env python |
| 2 | +""" Utility for git-bisecting nose failures |
| 3 | +""" |
| 4 | +DESCRIP = 'Check nose output for given text, set sys exit for git bisect' |
| 5 | +EPILOG = \ |
| 6 | +""" |
| 7 | +Imagine you've just detected a nose test failure. The failure is in a |
| 8 | +particular test or test module - here 'test_analyze.py'. The failure *is* in |
| 9 | +git branch ``main-master`` but it *is not* in tag ``v1.6.1``. Then you can |
| 10 | +bisect with something like:: |
| 11 | +
|
| 12 | + git co main-master |
| 13 | + git bisect start HEAD v1.6.1 -- |
| 14 | + git bisect run /path/to/bisect_nose.py nibabel/tests/test_analyze.py:TestAnalyzeImage.test_str |
| 15 | +
|
| 16 | +You might well want to test that:: |
| 17 | +
|
| 18 | + nosetests nibabel/tests/test_analyze.py:TestAnalyzeImage.test_str |
| 19 | +
|
| 20 | +works as you expect first. |
| 21 | +
|
| 22 | +Let's say instead that you prefer to recognize the failure with an output |
| 23 | +string. Maybe this is because there are lots of errors but you are only |
| 24 | +interested in one of them, or because you are looking for a Segmentation fault |
| 25 | +instead of a test failure. Then:: |
| 26 | +
|
| 27 | + git co main-master |
| 28 | + git bisect start HEAD v1.6.1 -- |
| 29 | + git bisect run /path/to/bisect_nose.py --error-txt='HeaderDataError: data dtype "int64" not recognized' nibabel/tests/test_analyze.py |
| 30 | +
|
| 31 | +where ``error-txt`` is in fact a regular expression. |
| 32 | +
|
| 33 | +You will need 'argparse' installed somewhere. This is in the system libraries |
| 34 | +for python 2.7 and python 3.2 onwards. |
| 35 | +
|
| 36 | +We run the tests in a temporary directory, so the code you are testing must be |
| 37 | +on the python path. |
| 38 | +""" |
| 39 | +import os |
| 40 | +import sys |
| 41 | +import shutil |
| 42 | +import tempfile |
| 43 | +import re |
| 44 | +from functools import partial |
| 45 | +from subprocess import check_call, Popen, PIPE, CalledProcessError |
| 46 | + |
| 47 | +from argparse import ArgumentParser, RawDescriptionHelpFormatter |
| 48 | + |
| 49 | +caller = partial(check_call, shell=True) |
| 50 | +popener = partial(Popen, stdout=PIPE, stderr=PIPE, shell=True) |
| 51 | + |
| 52 | +# git bisect exit codes |
| 53 | +UNTESTABLE = 125 |
| 54 | +GOOD = 0 |
| 55 | +BAD = 1 |
| 56 | + |
| 57 | +def call_or_untestable(cmd): |
| 58 | + try: |
| 59 | + caller(cmd) |
| 60 | + except CalledProcessError: |
| 61 | + sys.exit(UNTESTABLE) |
| 62 | + |
| 63 | + |
| 64 | +def main(): |
| 65 | + parser = ArgumentParser(description=DESCRIP, |
| 66 | + epilog=EPILOG, |
| 67 | + formatter_class=RawDescriptionHelpFormatter) |
| 68 | + parser.add_argument('test_path', type=str, |
| 69 | + help='Path to test') |
| 70 | + parser.add_argument('--error-txt', type=str, |
| 71 | + help='regular expression for error of interest') |
| 72 | + parser.add_argument('--clean', action='store_true', |
| 73 | + help='Clean git tree before running tests') |
| 74 | + parser.add_argument('--build', action='store_true', |
| 75 | + help='Build git tree before running tests') |
| 76 | + # parse the command line |
| 77 | + args = parser.parse_args() |
| 78 | + path = os.path.abspath(args.test_path) |
| 79 | + if args.clean: |
| 80 | + print "Cleaning" |
| 81 | + call_or_untestable('git clean -fxd') |
| 82 | + if args.build: |
| 83 | + print "Building" |
| 84 | + call_or_untestable('python setup.py build_ext -i') |
| 85 | + cwd = os.getcwd() |
| 86 | + tmpdir = tempfile.mkdtemp() |
| 87 | + try: |
| 88 | + os.chdir(tmpdir) |
| 89 | + print "Testing" |
| 90 | + proc = popener('nosetests ' + path) |
| 91 | + stdout, stderr = proc.communicate() |
| 92 | + finally: |
| 93 | + os.chdir(cwd) |
| 94 | + shutil.rmtree(tmpdir) |
| 95 | + if args.error_txt: |
| 96 | + regex = re.compile(args.error_txt) |
| 97 | + if regex.search(stderr): |
| 98 | + sys.exit(BAD) |
| 99 | + sys.exit(GOOD) |
| 100 | + sys.exit(proc.returncode) |
| 101 | + |
| 102 | + |
| 103 | +if __name__ == '__main__': |
| 104 | + main() |
0 commit comments