blob: 57e58f2eebb06c089b1fed3da60669000227a5e5 [file] [log] [blame]
Sean Dague50af5d52014-05-02 14:48:44 -04001#!/usr/bin/env python
2
3# Copyright 2014 Hewlett-Packard Development Company, L.P.
4# Copyright 2014 Samsung Electronics
5# All Rights Reserved.
6#
7# Licensed under the Apache License, Version 2.0 (the "License"); you may
8# not use this file except in compliance with the License. You may obtain
9# a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16# License for the specific language governing permissions and limitations
17# under the License.
18
19"""Trace a subunit stream in reasonable detail and high accuracy."""
20
Matthew Treinish1d5f32b2014-06-04 15:37:58 -040021import argparse
Sean Dague50af5d52014-05-02 14:48:44 -040022import functools
Sean Dague04867442014-05-06 08:51:15 -040023import re
Sean Dague50af5d52014-05-02 14:48:44 -040024import sys
25
Sean Dague50af5d52014-05-02 14:48:44 -040026import subunit
27import testtools
28
29DAY_SECONDS = 60 * 60 * 24
30FAILS = []
31RESULTS = {}
32
33
Sean Dague50af5d52014-05-02 14:48:44 -040034def cleanup_test_name(name, strip_tags=True, strip_scenarios=False):
35 """Clean up the test name for display.
36
37 By default we strip out the tags in the test because they don't help us
38 in identifying the test that is run to it's result.
39
40 Make it possible to strip out the testscenarios information (not to
41 be confused with tempest scenarios) however that's often needed to
42 indentify generated negative tests.
43 """
44 if strip_tags:
45 tags_start = name.find('[')
46 tags_end = name.find(']')
47 if tags_start > 0 and tags_end > tags_start:
48 newname = name[:tags_start]
49 newname += name[tags_end + 1:]
50 name = newname
51
52 if strip_scenarios:
53 tags_start = name.find('(')
54 tags_end = name.find(')')
55 if tags_start > 0 and tags_end > tags_start:
56 newname = name[:tags_start]
57 newname += name[tags_end + 1:]
58 name = newname
59
60 return name
61
62
63def get_duration(timestamps):
64 start, end = timestamps
65 if not start or not end:
66 duration = ''
67 else:
68 delta = end - start
69 duration = '%d.%06ds' % (
70 delta.days * DAY_SECONDS + delta.seconds, delta.microseconds)
71 return duration
72
73
74def find_worker(test):
75 for tag in test['tags']:
76 if tag.startswith('worker-'):
77 return int(tag[7:])
78 return 'NaN'
79
80
81# Print out stdout/stderr if it exists, always
82def print_attachments(stream, test, all_channels=False):
83 """Print out subunit attachments.
84
85 Print out subunit attachments that contain content. This
86 runs in 2 modes, one for successes where we print out just stdout
87 and stderr, and an override that dumps all the attachments.
88 """
89 channels = ('stdout', 'stderr')
90 for name, detail in test['details'].items():
91 # NOTE(sdague): the subunit names are a little crazy, and actually
92 # are in the form pythonlogging:'' (with the colon and quotes)
93 name = name.split(':')[0]
94 if detail.content_type.type == 'test':
95 detail.content_type.type = 'text'
96 if (all_channels or name in channels) and detail.as_text():
97 title = "Captured %s:" % name
98 stream.write("\n%s\n%s\n" % (title, ('~' * len(title))))
99 # indent attachment lines 4 spaces to make them visually
100 # offset
101 for line in detail.as_text().split('\n'):
102 stream.write(" %s\n" % line)
103
104
Matthew Treinish1d5f32b2014-06-04 15:37:58 -0400105def show_outcome(stream, test, print_failures=False):
Sean Dague50af5d52014-05-02 14:48:44 -0400106 global RESULTS
107 status = test['status']
108 # TODO(sdague): ask lifeless why on this?
109 if status == 'exists':
110 return
111
112 worker = find_worker(test)
113 name = cleanup_test_name(test['id'])
114 duration = get_duration(test['timestamps'])
115
116 if worker not in RESULTS:
117 RESULTS[worker] = []
118 RESULTS[worker].append(test)
119
120 # don't count the end of the return code as a fail
121 if name == 'process-returncode':
122 return
123
124 if status == 'success':
125 stream.write('{%s} %s [%s] ... ok\n' % (
126 worker, name, duration))
127 print_attachments(stream, test)
128 elif status == 'fail':
129 FAILS.append(test)
130 stream.write('{%s} %s [%s] ... FAILED\n' % (
131 worker, name, duration))
Matthew Treinish1d5f32b2014-06-04 15:37:58 -0400132 if not print_failures:
133 print_attachments(stream, test, all_channels=True)
Sean Dague50af5d52014-05-02 14:48:44 -0400134 elif status == 'skip':
135 stream.write('{%s} %s ... SKIPPED: %s\n' % (
136 worker, name, test['details']['reason'].as_text()))
137 else:
138 stream.write('{%s} %s [%s] ... %s\n' % (
139 worker, name, duration, test['status']))
Matthew Treinish1d5f32b2014-06-04 15:37:58 -0400140 if not print_failures:
141 print_attachments(stream, test, all_channels=True)
Sean Dague50af5d52014-05-02 14:48:44 -0400142
143 stream.flush()
144
145
146def print_fails(stream):
147 """Print summary failure report.
148
149 Currently unused, however there remains debate on inline vs. at end
150 reporting, so leave the utility function for later use.
151 """
152 if not FAILS:
153 return
154 stream.write("\n==============================\n")
155 stream.write("Failed %s tests - output below:" % len(FAILS))
156 stream.write("\n==============================\n")
157 for f in FAILS:
158 stream.write("\n%s\n" % f['id'])
159 stream.write("%s\n" % ('-' * len(f['id'])))
160 print_attachments(stream, f, all_channels=True)
161 stream.write('\n')
162
163
Sean Dague04867442014-05-06 08:51:15 -0400164def count_tests(key, value):
165 count = 0
166 for k, v in RESULTS.items():
167 for item in v:
168 if key in item:
169 if re.search(value, item[key]):
170 count += 1
171 return count
172
173
Matthew Treinish53eef722014-06-12 17:35:10 -0400174def run_time():
175 runtime = 0.0
176 for k, v in RESULTS.items():
177 for test in v:
178 runtime += float(get_duration(test['timestamps']).strip('s'))
179 return runtime
180
181
Sean Dague04867442014-05-06 08:51:15 -0400182def worker_stats(worker):
183 tests = RESULTS[worker]
184 num_tests = len(tests)
185 delta = tests[-1]['timestamps'][1] - tests[0]['timestamps'][0]
186 return num_tests, delta
187
188
189def print_summary(stream):
190 stream.write("\n======\nTotals\n======\n")
Matthew Treinish53eef722014-06-12 17:35:10 -0400191 stream.write("Run: %s in %s sec.\n" % (count_tests('status', '.*'),
192 run_time()))
Sean Dague04867442014-05-06 08:51:15 -0400193 stream.write(" - Passed: %s\n" % count_tests('status', 'success'))
194 stream.write(" - Skipped: %s\n" % count_tests('status', 'skip'))
195 stream.write(" - Failed: %s\n" % count_tests('status', 'fail'))
196
197 # we could have no results, especially as we filter out the process-codes
198 if RESULTS:
199 stream.write("\n==============\nWorker Balance\n==============\n")
200
201 for w in range(max(RESULTS.keys()) + 1):
202 if w not in RESULTS:
203 stream.write(
204 " - WARNING: missing Worker %s! "
205 "Race in testr accounting.\n" % w)
206 else:
207 num, time = worker_stats(w)
208 stream.write(" - Worker %s (%s tests) => %ss\n" %
209 (w, num, time))
210
211
Matthew Treinish1d5f32b2014-06-04 15:37:58 -0400212def parse_args():
213 parser = argparse.ArgumentParser()
214 parser.add_argument('--no-failure-debug', '-n', action='store_true',
215 dest='print_failures', help='Disable printing failure '
Robert Mizielskie1d88992014-07-15 15:28:09 +0200216 'debug information in realtime')
Matthew Treinish1d5f32b2014-06-04 15:37:58 -0400217 parser.add_argument('--fails', '-f', action='store_true',
218 dest='post_fails', help='Print failure debug '
219 'information after the stream is proccesed')
220 return parser.parse_args()
221
222
Sean Dague50af5d52014-05-02 14:48:44 -0400223def main():
Matthew Treinish1d5f32b2014-06-04 15:37:58 -0400224 args = parse_args()
Sean Dague50af5d52014-05-02 14:48:44 -0400225 stream = subunit.ByteStreamToStreamResult(
226 sys.stdin, non_subunit_name='stdout')
Sean Dague50af5d52014-05-02 14:48:44 -0400227 outcomes = testtools.StreamToDict(
Matthew Treinish1d5f32b2014-06-04 15:37:58 -0400228 functools.partial(show_outcome, sys.stdout,
229 print_failures=args.print_failures))
Sean Dague50af5d52014-05-02 14:48:44 -0400230 summary = testtools.StreamSummary()
Robert Collinsb5e4a982014-05-07 12:33:17 +1200231 result = testtools.CopyStreamResult([outcomes, summary])
Sean Dague50af5d52014-05-02 14:48:44 -0400232 result.startTestRun()
233 try:
234 stream.run(result)
235 finally:
236 result.stopTestRun()
Matthew Treinish178cc4a2014-09-12 18:54:34 -0400237 if count_tests('status', '.*') == 0:
238 print("The test run didn't actually run any tests")
239 return 1
Matthew Treinish1d5f32b2014-06-04 15:37:58 -0400240 if args.post_fails:
241 print_fails(sys.stdout)
Sean Dague04867442014-05-06 08:51:15 -0400242 print_summary(sys.stdout)
Sean Dague50af5d52014-05-02 14:48:44 -0400243 return (0 if summary.wasSuccessful() else 1)
244
245
246if __name__ == '__main__':
247 sys.exit(main())