Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(137)

Side by Side Diff: scripts/common/annotator.py

Issue 12386016: Initial implementation of annotator library. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Add unit tests, refactor step classes for protection. Created 7 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 #!/usr/bin/env python
2 # Copyright (c) 2013 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 """Contains generating and parsing systems of the Chromium Buildbot Annotator.
7
8 When executed as a script, this reads step name / command pairs from a file and
9 executes those lines while annotating the output. The input is json:
10
11 [{"name": "step_name", "cmd": ["command", "arg1", "arg2"]},
12 {"name": "step_name2", "cmd": ["command2", "arg1"]}]
13
14 """
15
16 import json
17 import optparse
18 import re
19 import sys
20 import traceback
21
22 from common import chromium_utils
23
24
25 class StepCommands(object):
26 """Subclass holding step commands. Intended to be subclassed."""
agable 2013/03/02 00:49:56 """Class holding..."""
27 def __init__(self, stream):
28 self.stream = stream
29
30 def emit(self, line):
31 print >> self.stream, line
32
33 def step_warnings(self):
34 self.emit('@@@STEP_WARNINGS@@@')
35
36 def step_failure(self):
37 self.emit('@@@STEP_FAILURE@@@')
38
39 def step_exception(self):
40 self.emit('@@@STEP_EXCEPTION@@@')
41
42 def step_clear(self):
43 self.emit('@@@STEP_CLEAR@@@')
44
45 def step_summary_clear(self):
46 self.emit('@@@STEP_SUMMARY_CLEAR@@@')
47
48 def step_text(self, text):
49 self.emit('@@@STEP_TEXT@%s@@@' % text)
50
51 def step_summary_text(self, text):
52 self.emit('@@@STEP_SUMMARY_TEXT@%s@@@' % text)
53
54 def step_log_line(self, logname, line):
55 self.emit('@@@STEP_LOG_LINE@%s@%s@@@' % (logname, line.rstrip('\n')))
56
57 def step_log_end(self, logname):
58 self.emit('@@@STEP_LOG_END@%s@@@' % logname)
59
60 def step_log_end_perf(self, logname, perf):
61 self.emit('@@@STEP_LOG_END_PERF@%s@%s@@@' % (logname, perf))
62
63 def write_log_lines(self, logname, lines, perf=None):
64 for line in lines:
65 self.step_log_line(logname, line)
66 if perf:
67 self.step_log_end_perf(logname, perf)
68 else:
69 self.step_log_end(logname)
70
71
72 class StepControlCommands(object):
73 """Subclass holding step control commands. Intended to be subclassed.
74
75 This is subclassed out so callers in StructuredAnnotationStep can't call
76 step_started() or step_closed().
77
78 """
79 def __init__(self, stream):
80 self.stream = stream
81
82 def emit(self, line):
iannucci 2013/03/02 01:49:14 Seems like there should be a way to only have 'emi
83 print >> self.stream, line
84
85 def step_started(self):
86 self.emit('@@@STEP_STARTED@@@')
87
88 def step_closed(self):
89 self.emit('@@@STEP_CLOSED@@@')
90
91
92 class StructuredAnnotationStep(StepCommands):
93 """Helper class to provide context for a step."""
94
95 def __init__(self, annotation_stream, *args, **kwargs):
96 self.annotation_stream = annotation_stream
97 super(StructuredAnnotationStep, self).__init__(*args, **kwargs)
98 self.control = StepControlCommands(self.stream)
99
100 def emit(self, line):
101 print >> self.stream, line
102
103 def __enter__(self):
104 self.control.step_started()
105 return self
106
107 def __exit__(self, exc_type, exc_value, tb):
108 if exc_type:
109 trace = traceback.format_exception(exc_type, exc_value, tb)
110 unflattened = [l.split('\n') for l in trace]
111 flattened = [item for sublist in unflattened for item in sublist]
iannucci 2013/03/02 01:49:14 Maybe "".join(trace).split('\n')
112 self.write_log_lines('exception', filter(None, flattened))
113 self.step_exception()
114
115 self.control.step_closed()
116 self.annotation_stream.current_step = ''
117 return not exc_type
118
119 class AdvancedAnnotationStep(StepCommands, StepControlCommands):
120 """Holds additional step functions for finer step control.
121
122 Most users will want to use StructuredAnnotationSteps generated from a
123 StructuredAnnotationStream as these handle state automatically.
124 """
125
126 def __init__(self, *args, **kwargs):
127 super(AdvancedAnnotationStep, self).__init__(*args, **kwargs)
128
129
130 class AdvancedAnnotationStream(object):
131 """Holds individual annotation generating functions for streams.
132
133 Most callers should use StructuredAnnotationStream to simplify coding and
134 avoid errors. For the rare cases where StructuredAnnotationStream is
135 insufficient (parallel step execution), the indidividual functions are exposed
136 here.
137
agable 2013/03/02 00:49:56 No newline.
138 """
139
140 def __init__(self, stream=sys.stdout):
141 self.stream = stream
142
143 def emit(self, line):
144 print >> self.stream, line
145
146 def seed_step(self, step):
147 self.emit('@@@SEED_STEP %s@@@' % step)
148
149 def step_cursor(self, step):
150 self.emit('@@@STEP_CURSOR %s@@@' % step)
151
152 def halt_on_failure(self):
153 self.emit('@@@HALT_ON_FAILURE@@@')
154
155 def honor_zero_return_code(self):
156 self.emit('@@@HONOR_ZERO_RETURN_CODE@@@')
157
158
159 class StructuredAnnotationStream(AdvancedAnnotationStream):
160 """Provides an interface to handle an annotated build.
161
162 StructuredAnnotationStream handles most of the step setup and closure calls
163 for you. All you have to do is execute your code within the steps and set any
164 failures or warnings that come up. You may optionally provide a list of steps
165 to seed before execution.
166
167 Usage:
168
169 stream = StructuredAnnotationStream()
170 with stream.step('compile') as s:
171 # do something
172 if error:
173 s.step_failure()
174 with stream.step('test') as s:
175 # do something
176 if warnings:
177 s.step_warnings()
178 """
179
180 def __init__(self, seed_steps=None, stream=sys.stdout):
181 super(StructuredAnnotationStream, self).__init__(stream=stream)
182 seed_steps = seed_steps or []
183 self.seed_steps = seed_steps
184
185 for step in seed_steps:
186 self.seed_step(step)
187
188 self.current_step = ''
189
190 def step(self, name):
191 """Provide a context with which to execute a step."""
192 if self.current_step:
193 raise Exception('Can\'t start step %s while in step %s.' % (
194 name, self.current_step))
195 if name in self.seed_steps:
196 # Seek ahead linearly, skipping steps that weren't emitted in order.
197 # chromium_step.AnnotatedCommands uses the last in case of duplicated
198 # step names, so we do the same here.
199 idx = len(self.seed_steps) - self.seed_steps[::-1].index(name)
200 self.seed_steps = self.seed_steps[idx:]
201 else:
202 self.seed_step(name)
203
204 self.step_cursor(name)
205 self.current_step = name
206 return StructuredAnnotationStep(self, stream=self.stream)
207
208
209 class Match:
210 """Holds annotator line parsing functions."""
211
212 def __init__(self):
213 raise Exception('Don\'t instantiate the Match class!')
214
215 @staticmethod
216 def _parse_line(regex, line):
217 m = re.match(regex, line)
218 if m:
219 return list(m.groups())
220 else:
221 return []
222
223 @staticmethod
224 def log_line(line):
225 return Match._parse_line('^@@@STEP_LOG_LINE@(.*)@(.*)@@@', line)
226
227 @staticmethod
228 def log_end(line):
229 return Match._parse_line('^@@@STEP_LOG_END@(.*)@@@', line)
230
231 @staticmethod
232 def log_end_perf(line):
233 return Match._parse_line('^@@@STEP_LOG_END_PERF@(.*)@(.*)@@@', line)
234
235 @staticmethod
236 def step_link(line):
237 m = Match._parse_line('^@@@STEP_LINK@(.*)@(.*)@@@', line)
238 if not m:
239 return Match._parse_line('^@@@link@(.*)@(.*)@@@', line) # Deprecated.
240 else:
241 return m
242
243 @staticmethod
244 def step_started(line):
245 return line.startswith('@@@STEP_STARTED@@@')
246
247 @staticmethod
248 def step_closed(line):
249 return line.startswith('@@@STEP_CLOSED@@@')
250
251 @staticmethod
252 def step_warnings(line):
253 return (line.startswith('@@@STEP_WARNINGS@@@') or
254 line.startswith('@@@BUILD_WARNINGS@@@')) # Deprecated.
255
256 @staticmethod
257 def step_failure(line):
258 return (line.startswith('@@@STEP_FAILURE@@@') or
259 line.startswith('@@@BUILD_FAILED@@@')) # Deprecated.
260
261 @staticmethod
262 def step_exception(line):
263 return (line.startswith('@@@STEP_EXCEPTION@@@') or
264 line.startswith('@@@BUILD_EXCEPTION@@@')) # Deprecated.
265
266 @staticmethod
267 def halt_on_failure(line):
268 return line.startswith('@@@HALT_ON_FAILURE@@@')
269
270 @staticmethod
271 def honor_zero_return_code(line):
272 return line.startswith('@@@HONOR_ZERO_RETURN_CODE@@@')
273
274 @staticmethod
275 def step_clear(line):
276 return line.startswith('@@@STEP_CLEAR@@@')
277
278 @staticmethod
279 def step_summary_clear(line):
280 return line.startswith('@@@STEP_SUMMARY_CLEAR@@@')
281
282 @staticmethod
283 def step_text(line):
284 return Match._parse_line('^@@@STEP_TEXT@(.*)@@@', line)
285
286 @staticmethod
287 def step_summary_text(line):
288 return Match._parse_line('^@@@STEP_SUMMARY_TEXT@(.*)@@@', line)
289
290 @staticmethod
291 def seed_step(line):
292 return Match._parse_line('^@@@SEED_STEP (.*)@@@', line)
293
294 @staticmethod
295 def step_cursor(line):
296 return Match._parse_line('^@@@STEP_CURSOR (.*)@@@', line)
297
298 @staticmethod
299 def build_step(line):
300 return Match._parse_line('^@@@BUILD_STEP (.*)@@@', line)
301
302
303 def main():
304 usage = '%s <command list file>' % sys.argv[0]
305 parser = optparse.OptionParser(usage=usage)
306 _, args = parser.parse_args()
307 if not args:
308 parser.error('Must specify an input filename!')
iannucci 2013/03/02 01:49:14 Check for len(args) > 1?
309
310 steps = []
311 with open(args[0], 'rb') as f:
312 steps.extend(json.load(f))
313
314 for step in steps:
315 if ('cmd' not in step or
316 'name' not in step):
317 print 'step \'%s\' is invalid' % json.dumps(step)
318 return 1
319
320 # Make sure these steps always run, even if there is a build failure.
321 always_run = {}
322 for step in steps:
323 if step.get('always_run'):
324 always_run[step['name']] = step
325
326 stepnames = [s['name'] for s in steps]
327
328 stream = StructuredAnnotationStream(seed_steps=stepnames)
329 build_failure = False
330 for step in steps:
331 if step['name'] in always_run:
332 del always_run[step['name']]
333 try:
334 with stream.step(step['name']) as s:
335 ret = chromium_utils.RunCommand(step['cmd'])
336 if ret != 0:
337 s.step_failure()
338 build_failure = True
339 break
340 except OSError:
341 # File wasn't found, error has been already reported to stream.
342 build_failure = True
343 break
344
345 for step_name in always_run:
346 with stream.step(step_name) as s:
347 ret = chromium_utils.RunCommand(always_run[step_name]['cmd'])
348 if ret != 0:
349 s.step_failure()
350 build_failure = True
351
352 if build_failure:
353 return 1
354 return 0
355
356
357 if __name__ == '__main__':
358 sys.exit(main())
OLDNEW
« no previous file with comments | « no previous file | scripts/common/unittests/annotator_test.py » ('j') | scripts/common/unittests/annotator_test.py » ('J')

Powered by Google App Engine
This is Rietveld 408576698