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