blob: 8ad59bbabb89ae33623f897a39a11330cb980872 [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
26import mimeparse
27import subunit
28import testtools
29
30DAY_SECONDS = 60 * 60 * 24
31FAILS = []
32RESULTS = {}
33
34
35class Starts(testtools.StreamResult):
36
37 def __init__(self, output):
38 super(Starts, self).__init__()
39 self._output = output
40
41 def startTestRun(self):
42 self._neednewline = False
43 self._emitted = set()
44
45 def status(self, test_id=None, test_status=None, test_tags=None,
46 runnable=True, file_name=None, file_bytes=None, eof=False,
47 mime_type=None, route_code=None, timestamp=None):
48 super(Starts, self).status(
49 test_id, test_status,
50 test_tags=test_tags, runnable=runnable, file_name=file_name,
51 file_bytes=file_bytes, eof=eof, mime_type=mime_type,
52 route_code=route_code, timestamp=timestamp)
53 if not test_id:
54 if not file_bytes:
55 return
56 if not mime_type or mime_type == 'test/plain;charset=utf8':
57 mime_type = 'text/plain; charset=utf-8'
58 primary, sub, parameters = mimeparse.parse_mime_type(mime_type)
59 content_type = testtools.content_type.ContentType(
60 primary, sub, parameters)
61 content = testtools.content.Content(
62 content_type, lambda: [file_bytes])
63 text = content.as_text()
64 if text and text[-1] not in '\r\n':
65 self._neednewline = True
66 self._output.write(text)
67 elif test_status == 'inprogress' and test_id not in self._emitted:
68 if self._neednewline:
69 self._neednewline = False
70 self._output.write('\n')
71 worker = ''
72 for tag in test_tags or ():
73 if tag.startswith('worker-'):
74 worker = '(' + tag[7:] + ') '
75 if timestamp:
76 timestr = timestamp.isoformat()
77 else:
78 timestr = ''
79 self._output.write('%s: %s%s [start]\n' %
80 (timestr, worker, test_id))
81 self._emitted.add(test_id)
82
83
84def cleanup_test_name(name, strip_tags=True, strip_scenarios=False):
85 """Clean up the test name for display.
86
87 By default we strip out the tags in the test because they don't help us
88 in identifying the test that is run to it's result.
89
90 Make it possible to strip out the testscenarios information (not to
91 be confused with tempest scenarios) however that's often needed to
92 indentify generated negative tests.
93 """
94 if strip_tags:
95 tags_start = name.find('[')
96 tags_end = name.find(']')
97 if tags_start > 0 and tags_end > tags_start:
98 newname = name[:tags_start]
99 newname += name[tags_end + 1:]
100 name = newname
101
102 if strip_scenarios:
103 tags_start = name.find('(')
104 tags_end = name.find(')')
105 if tags_start > 0 and tags_end > tags_start:
106 newname = name[:tags_start]
107 newname += name[tags_end + 1:]
108 name = newname
109
110 return name
111
112
113def get_duration(timestamps):
114 start, end = timestamps
115 if not start or not end:
116 duration = ''
117 else:
118 delta = end - start
119 duration = '%d.%06ds' % (
120 delta.days * DAY_SECONDS + delta.seconds, delta.microseconds)
121 return duration
122
123
124def find_worker(test):
125 for tag in test['tags']:
126 if tag.startswith('worker-'):
127 return int(tag[7:])
128 return 'NaN'
129
130
131# Print out stdout/stderr if it exists, always
132def print_attachments(stream, test, all_channels=False):
133 """Print out subunit attachments.
134
135 Print out subunit attachments that contain content. This
136 runs in 2 modes, one for successes where we print out just stdout
137 and stderr, and an override that dumps all the attachments.
138 """
139 channels = ('stdout', 'stderr')
140 for name, detail in test['details'].items():
141 # NOTE(sdague): the subunit names are a little crazy, and actually
142 # are in the form pythonlogging:'' (with the colon and quotes)
143 name = name.split(':')[0]
144 if detail.content_type.type == 'test':
145 detail.content_type.type = 'text'
146 if (all_channels or name in channels) and detail.as_text():
147 title = "Captured %s:" % name
148 stream.write("\n%s\n%s\n" % (title, ('~' * len(title))))
149 # indent attachment lines 4 spaces to make them visually
150 # offset
151 for line in detail.as_text().split('\n'):
152 stream.write(" %s\n" % line)
153
154
Matthew Treinish1d5f32b2014-06-04 15:37:58 -0400155def show_outcome(stream, test, print_failures=False):
Sean Dague50af5d52014-05-02 14:48:44 -0400156 global RESULTS
157 status = test['status']
158 # TODO(sdague): ask lifeless why on this?
159 if status == 'exists':
160 return
161
162 worker = find_worker(test)
163 name = cleanup_test_name(test['id'])
164 duration = get_duration(test['timestamps'])
165
166 if worker not in RESULTS:
167 RESULTS[worker] = []
168 RESULTS[worker].append(test)
169
170 # don't count the end of the return code as a fail
171 if name == 'process-returncode':
172 return
173
174 if status == 'success':
175 stream.write('{%s} %s [%s] ... ok\n' % (
176 worker, name, duration))
177 print_attachments(stream, test)
178 elif status == 'fail':
179 FAILS.append(test)
180 stream.write('{%s} %s [%s] ... FAILED\n' % (
181 worker, name, duration))
Matthew Treinish1d5f32b2014-06-04 15:37:58 -0400182 if not print_failures:
183 print_attachments(stream, test, all_channels=True)
Sean Dague50af5d52014-05-02 14:48:44 -0400184 elif status == 'skip':
185 stream.write('{%s} %s ... SKIPPED: %s\n' % (
186 worker, name, test['details']['reason'].as_text()))
187 else:
188 stream.write('{%s} %s [%s] ... %s\n' % (
189 worker, name, duration, test['status']))
Matthew Treinish1d5f32b2014-06-04 15:37:58 -0400190 if not print_failures:
191 print_attachments(stream, test, all_channels=True)
Sean Dague50af5d52014-05-02 14:48:44 -0400192
193 stream.flush()
194
195
196def print_fails(stream):
197 """Print summary failure report.
198
199 Currently unused, however there remains debate on inline vs. at end
200 reporting, so leave the utility function for later use.
201 """
202 if not FAILS:
203 return
204 stream.write("\n==============================\n")
205 stream.write("Failed %s tests - output below:" % len(FAILS))
206 stream.write("\n==============================\n")
207 for f in FAILS:
208 stream.write("\n%s\n" % f['id'])
209 stream.write("%s\n" % ('-' * len(f['id'])))
210 print_attachments(stream, f, all_channels=True)
211 stream.write('\n')
212
213
Sean Dague04867442014-05-06 08:51:15 -0400214def count_tests(key, value):
215 count = 0
216 for k, v in RESULTS.items():
217 for item in v:
218 if key in item:
219 if re.search(value, item[key]):
220 count += 1
221 return count
222
223
Matthew Treinish53eef722014-06-12 17:35:10 -0400224def run_time():
225 runtime = 0.0
226 for k, v in RESULTS.items():
227 for test in v:
228 runtime += float(get_duration(test['timestamps']).strip('s'))
229 return runtime
230
231
Sean Dague04867442014-05-06 08:51:15 -0400232def worker_stats(worker):
233 tests = RESULTS[worker]
234 num_tests = len(tests)
235 delta = tests[-1]['timestamps'][1] - tests[0]['timestamps'][0]
236 return num_tests, delta
237
238
239def print_summary(stream):
240 stream.write("\n======\nTotals\n======\n")
Matthew Treinish53eef722014-06-12 17:35:10 -0400241 stream.write("Run: %s in %s sec.\n" % (count_tests('status', '.*'),
242 run_time()))
Sean Dague04867442014-05-06 08:51:15 -0400243 stream.write(" - Passed: %s\n" % count_tests('status', 'success'))
244 stream.write(" - Skipped: %s\n" % count_tests('status', 'skip'))
245 stream.write(" - Failed: %s\n" % count_tests('status', 'fail'))
246
247 # we could have no results, especially as we filter out the process-codes
248 if RESULTS:
249 stream.write("\n==============\nWorker Balance\n==============\n")
250
251 for w in range(max(RESULTS.keys()) + 1):
252 if w not in RESULTS:
253 stream.write(
254 " - WARNING: missing Worker %s! "
255 "Race in testr accounting.\n" % w)
256 else:
257 num, time = worker_stats(w)
258 stream.write(" - Worker %s (%s tests) => %ss\n" %
259 (w, num, time))
260
261
Matthew Treinish1d5f32b2014-06-04 15:37:58 -0400262def parse_args():
263 parser = argparse.ArgumentParser()
264 parser.add_argument('--no-failure-debug', '-n', action='store_true',
265 dest='print_failures', help='Disable printing failure '
Robert Mizielskie1d88992014-07-15 15:28:09 +0200266 'debug information in realtime')
Matthew Treinish1d5f32b2014-06-04 15:37:58 -0400267 parser.add_argument('--fails', '-f', action='store_true',
268 dest='post_fails', help='Print failure debug '
269 'information after the stream is proccesed')
270 return parser.parse_args()
271
272
Sean Dague50af5d52014-05-02 14:48:44 -0400273def main():
Matthew Treinish1d5f32b2014-06-04 15:37:58 -0400274 args = parse_args()
Sean Dague50af5d52014-05-02 14:48:44 -0400275 stream = subunit.ByteStreamToStreamResult(
276 sys.stdin, non_subunit_name='stdout')
277 starts = Starts(sys.stdout)
278 outcomes = testtools.StreamToDict(
Matthew Treinish1d5f32b2014-06-04 15:37:58 -0400279 functools.partial(show_outcome, sys.stdout,
280 print_failures=args.print_failures))
Sean Dague50af5d52014-05-02 14:48:44 -0400281 summary = testtools.StreamSummary()
282 result = testtools.CopyStreamResult([starts, outcomes, summary])
283 result.startTestRun()
284 try:
285 stream.run(result)
286 finally:
287 result.stopTestRun()
Matthew Treinish1d5f32b2014-06-04 15:37:58 -0400288 if args.post_fails:
289 print_fails(sys.stdout)
Sean Dague04867442014-05-06 08:51:15 -0400290 print_summary(sys.stdout)
Sean Dague50af5d52014-05-02 14:48:44 -0400291 return (0 if summary.wasSuccessful() else 1)
292
293
294if __name__ == '__main__':
295 sys.exit(main())