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