Browse code

Replace old shippable.py with new check_matrix.py. (#60022)

This new script does not depend on ansible-test and provides much more robust job matrix testing.

It is also run on every job in the matrix now, to detect issues with jobs being re-run after matrix changes are made.

(cherry picked from commit d3da8e4a5b7c87ea6bb4f1345300ddb0a833a6b2)

Matt Clay authored on 2019/08/06 03:27:37
Showing 5 changed files
1 1
deleted file mode 100755
... ...
@@ -1,107 +0,0 @@
1
-#!/usr/bin/env python
2
-# PYTHON_ARGCOMPLETE_OK
3
-"""Verify the current Shippable run has the required number of jobs."""
4
-
5
-from __future__ import absolute_import, print_function
6
-
7
-# noinspection PyCompatibility
8
-import argparse
9
-import errno
10
-import json
11
-import os
12
-import sys
13
-
14
-from lib.http import (
15
-    HttpClient,
16
-)
17
-
18
-from lib.util import (
19
-    display,
20
-    ApplicationError,
21
-    ApplicationWarning,
22
-    MissingEnvironmentVariable,
23
-)
24
-
25
-
26
-try:
27
-    import argcomplete
28
-except ImportError:
29
-    argcomplete = None
30
-
31
-
32
-def main():
33
-    """Main program function."""
34
-    try:
35
-        args = parse_args()
36
-        display.verbosity = args.verbosity
37
-        display.color = args.color
38
-
39
-        try:
40
-            run_id = os.environ['SHIPPABLE_BUILD_ID']
41
-        except KeyError as ex:
42
-            raise MissingEnvironmentVariable(ex.args[0])
43
-
44
-        client = HttpClient(args)
45
-        response = client.get('https://api.shippable.com/jobs?runIds=%s' % run_id)
46
-        jobs = response.json()
47
-
48
-        if not isinstance(jobs, list):
49
-            raise ApplicationError(json.dumps(jobs, indent=4, sort_keys=True))
50
-
51
-        if len(jobs) == 1:
52
-            raise ApplicationError('Shippable run %s has only one job. Did you use the "Rebuild with SSH" option?' % run_id)
53
-    except ApplicationWarning as ex:
54
-        display.warning(str(ex))
55
-        exit(0)
56
-    except ApplicationError as ex:
57
-        display.error(str(ex))
58
-        exit(1)
59
-    except KeyboardInterrupt:
60
-        exit(2)
61
-    except IOError as ex:
62
-        if ex.errno == errno.EPIPE:
63
-            exit(3)
64
-        raise
65
-
66
-
67
-def parse_args():
68
-    """Parse command line arguments."""
69
-    parser = argparse.ArgumentParser()
70
-
71
-    parser.add_argument('-e', '--explain',
72
-                        action='store_true',
73
-                        help='explain commands that would be executed')
74
-
75
-    parser.add_argument('-v', '--verbose',
76
-                        dest='verbosity',
77
-                        action='count',
78
-                        default=0,
79
-                        help='display more output')
80
-
81
-    parser.add_argument('--color',
82
-                        metavar='COLOR',
83
-                        nargs='?',
84
-                        help='generate color output: %(choices)s',
85
-                        choices=('yes', 'no', 'auto'),
86
-                        const='yes',
87
-                        default='auto')
88
-
89
-    if argcomplete:
90
-        argcomplete.autocomplete(parser)
91
-
92
-    args = parser.parse_args()
93
-
94
-    if args.color == 'yes':
95
-        args.color = True
96
-    elif args.color == 'no':
97
-        args.color = False
98
-    elif 'SHIPPABLE' in os.environ:
99
-        args.color = True
100
-    else:
101
-        args.color = sys.stdout.isatty()
102
-
103
-    return args
104
-
105
-
106
-if __name__ == '__main__':
107
-    main()
... ...
@@ -12,6 +12,7 @@ def main():
12 12
         'lib/ansible/module_utils/urls.py',
13 13
         'test/units/module_utils/urls/test_Request.py',
14 14
         'test/units/module_utils/urls/test_fetch_url.py',
15
+        'test/utils/shippable/check_matrix.py',
15 16
     ])
16 17
 
17 18
     for path in sys.argv[1:] or sys.stdin.read().splitlines():
18 19
new file mode 100755
... ...
@@ -0,0 +1,107 @@
0
+#!/usr/bin/env python
1
+"""Verify the currently executing Shippable test matrix matches the one defined in the "shippable.yml" file."""
2
+from __future__ import (absolute_import, division, print_function)
3
+__metaclass__ = type
4
+
5
+import datetime
6
+import json
7
+import os
8
+import re
9
+import sys
10
+import time
11
+
12
+try:
13
+    from typing import NoReturn
14
+except ImportError:
15
+    NoReturn = None
16
+
17
+try:
18
+    # noinspection PyCompatibility
19
+    from urllib2 import urlopen  # pylint: disable=ansible-bad-import-from
20
+except ImportError:
21
+    # noinspection PyCompatibility
22
+    from urllib.request import urlopen
23
+
24
+
25
+def main():  # type: () -> None
26
+    """Main entry point."""
27
+    with open('shippable.yml', 'rb') as yaml_file:
28
+        yaml = yaml_file.read().decode('utf-8').splitlines()
29
+
30
+    defined_matrix = [match.group(1) for match in [re.search(r'^ *- env: T=(.*)$', line) for line in yaml] if match and match.group(1) != 'none']
31
+
32
+    if not defined_matrix:
33
+        fail('No matrix entries found in the "shippable.yml" file.',
34
+             'Did you modify the "shippable.yml" file?')
35
+
36
+    run_id = os.environ['SHIPPABLE_BUILD_ID']
37
+    sleep = 1
38
+    jobs = []
39
+
40
+    for attempts_remaining in range(4, -1, -1):
41
+        try:
42
+            jobs = json.loads(urlopen('https://api.shippable.com/jobs?runIds=%s' % run_id).read())
43
+
44
+            if not isinstance(jobs, list):
45
+                raise Exception('Shippable run %s data is not a list.' % run_id)
46
+
47
+            break
48
+        except Exception as ex:
49
+            if not attempts_remaining:
50
+                fail('Unable to retrieve Shippable run %s matrix.' % run_id,
51
+                     str(ex))
52
+
53
+            sys.stderr.write('Unable to retrieve Shippable run %s matrix: %s\n' % (run_id, ex))
54
+            sys.stderr.write('Trying again in %d seconds...\n' % sleep)
55
+            time.sleep(sleep)
56
+            sleep *= 2
57
+
58
+    if len(jobs) != len(defined_matrix):
59
+        if len(jobs) == 1:
60
+            hint = '\n\nMake sure you do not use the "Rebuild with SSH" option.'
61
+        else:
62
+            hint = ''
63
+
64
+        fail('Shippable run %s has %d jobs instead of the expected %d jobs.' % (run_id, len(jobs), len(defined_matrix)),
65
+             'Try re-running the entire matrix.%s' % hint)
66
+
67
+    actual_matrix = dict((job.get('jobNumber'), dict(tuple(line.split('=', 1)) for line in job.get('env', [])).get('T', '')) for job in jobs)
68
+    errors = [(job_number, test, actual_matrix.get(job_number)) for job_number, test in enumerate(defined_matrix, 1) if actual_matrix.get(job_number) != test]
69
+
70
+    if len(errors):
71
+        error_summary = '\n'.join('Job %s expected "%s" but found "%s" instead.' % (job_number, expected, actual) for job_number, expected, actual in errors)
72
+
73
+        fail('Shippable run %s has a job matrix mismatch.' % run_id,
74
+             'Try re-running the entire matrix.\n\n%s' % error_summary)
75
+
76
+
77
+def fail(message, output):  # type: (str, str) -> NoReturn
78
+    # Include a leading newline to improve readability on Shippable "Tests" tab.
79
+    # Without this, the first line becomes indented.
80
+    output = '\n' + output.strip()
81
+
82
+    timestamp = datetime.datetime.utcnow().replace(microsecond=0).isoformat()
83
+
84
+    # hack to avoid requiring junit-xml, which isn't pre-installed on Shippable outside our test containers
85
+    xml = '''
86
+<?xml version="1.0" encoding="utf-8"?>
87
+<testsuites disabled="0" errors="1" failures="0" tests="1" time="0.0">
88
+\t<testsuite disabled="0" errors="1" failures="0" file="None" log="None" name="ansible-test" skipped="0" tests="1" time="0" timestamp="%s" url="None">
89
+\t\t<testcase classname="timeout" name="timeout">
90
+\t\t\t<error message="%s" type="error">%s</error>
91
+\t\t</testcase>
92
+\t</testsuite>
93
+</testsuites>
94
+''' % (timestamp, message, output)
95
+
96
+    with open('test/results/junit/check-matrix.xml', 'w') as junit_fd:
97
+        junit_fd.write(xml.lstrip())
98
+
99
+    sys.stderr.write(message + '\n')
100
+    sys.stderr.write(output + '\n')
101
+
102
+    sys.exit(1)
103
+
104
+
105
+if __name__ == '__main__':
106
+    main()
... ...
@@ -7,8 +7,6 @@ IFS='/:' read -ra args <<< "$1"
7 7
 
8 8
 group="${args[1]}"
9 9
 
10
-shippable.py
11
-
12 10
 if [ "${BASE_BRANCH:-}" ]; then
13 11
     base_branch="origin/${BASE_BRANCH}"
14 12
 else
... ...
@@ -118,4 +118,5 @@ trap cleanup EXIT
118 118
 
119 119
 ansible-test env --dump --show --color -v
120 120
 
121
+"test/utils/shippable/check_matrix.py"
121 122
 "test/utils/shippable/${script}.sh" "${test}"