~launchpad-pqm/launchpad/devel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#!${buildout:executable}
#
# Copyright 2009 Canonical Ltd.  This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).

"""
Given an error report, run all of the failed tests again.

For instance, it can be used in the following scenario:

  % bin/test -vvm lp.registry | tee test.out
  % # Oh nos!  Failures!
  % # Fix tests.
  % bin/retest test.out

Or, when run without arguments (or if any argument is "-"), a test
report (or a part of) can be piped in, for example by pasting it:

  % bin/retest
  Tests with failures:
     lib/lp/registry/browser/tests/sourcepackage-views.txt
     lib/lp/registry/tests/../stories/product/xx-product-package-pages.txt
  Total: ... tests, 2 failures, 0 errors in ...

"""

import fileinput
import os
import re
import sys
from itertools import takewhile, imap

${python-relative-path-setup}

# The test script for this branch.
TEST = "${buildout:directory/bin/test}"

# Regular expression to match numbered stories.
STORY_RE = re.compile("(.*)/\d{2}-.*")

# Regular expression to remove terminal color escapes.
COLOR_RE = re.compile("\x1b[[][0-9;]+m")


def decolorize(text):
    """Remove all ANSI terminal color escapes from `text`."""
    return COLOR_RE.sub("", text)


def get_test_name(test):
    """Get the test name of a failed test.

    If the test is part of a numbered story,
    e.g. 'stories/gpg-coc/01-claimgpgp.txt', then return the directory name
    since all of the stories must be run together.
    """
    match = STORY_RE.match(test)
    if match:
        return match.group(1)
    else:
        return test


def gen_test_lines(lines):
    def p_start(line):
        return (
            line.startswith('Tests with failures:') or
            line.startswith('Tests with errors:'))
    def p_take(line):
        return not (
            line.isspace() or
            line.startswith('Total:'))
    lines = iter(lines)
    for line in lines:
        if p_start(line):
            for line in takewhile(p_take, lines):
                yield line


def gen_tests(test_lines):
    for test_line in test_lines:
        yield get_test_name(test_line.strip())


def extract_tests(lines):
    return set(gen_tests(gen_test_lines(lines)))


def run_tests(tests):
    """Given a set of tests, run them as one group."""
    print "Running tests:"
    for test in tests:
        print "  %s" % test
    args = ['-vvc'] if sys.stdout.isatty() else ['-vv']
    for test in tests:
        args.append('-t')
        args.append(re.escape(test))
    os.execl(TEST, TEST, *args)


if __name__ == '__main__':
    lines = imap(decolorize, fileinput.input())
    tests = extract_tests(lines)
    if len(tests) >= 1:
        run_tests(tests)
    else:
        sys.stdout.write(
            "Error: no tests found\n"
            "Usage: %s [test_output_file|-] ...\n\n%s\n\n" % (
                sys.argv[0], __doc__.strip()))
        sys.exit(1)