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

Side by Side Diff: third_party/recipe_engine/third_party/annotator.py

Issue 1241323004: Cross-repo recipe package system. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Change to googlesource Created 5 years, 3 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 # Copyright (c) 2015 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4
5 """Contains the parsing system of the Chromium Buildbot Annotator."""
6
7 import os
8 import sys
9 import traceback
10
11 # These are maps of annotation key -> number of expected arguments.
12 STEP_ANNOTATIONS = {
13 'SET_BUILD_PROPERTY': 2,
14 'STEP_CLEAR': 0,
15 'STEP_EXCEPTION': 0,
16 'STEP_FAILURE': 0,
17 'STEP_LINK': 2,
18 'STEP_LOG_END': 1,
19 'STEP_LOG_END_PERF': 2,
20 'STEP_LOG_LINE': 2,
21 'STEP_SUMMARY_CLEAR': 0,
22 'STEP_SUMMARY_TEXT': 1,
23 'STEP_TEXT': 1,
24 'STEP_TRIGGER': 1,
25 'STEP_WARNINGS': 0,
26 'STEP_NEST_LEVEL': 1,
27 }
28
29 CONTROL_ANNOTATIONS = {
30 'STEP_CLOSED': 0,
31 'STEP_STARTED': 0,
32 }
33
34 STREAM_ANNOTATIONS = {
35 'HALT_ON_FAILURE': 0,
36 'HONOR_ZERO_RETURN_CODE': 0,
37 'SEED_STEP': 1,
38 'SEED_STEP_TEXT': 2,
39 'STEP_CURSOR': 1,
40 }
41
42 DEPRECATED_ANNOTATIONS = {
43 'BUILD_STEP': 1,
44 }
45
46 ALL_ANNOTATIONS = {}
47 ALL_ANNOTATIONS.update(STEP_ANNOTATIONS)
48 ALL_ANNOTATIONS.update(CONTROL_ANNOTATIONS)
49 ALL_ANNOTATIONS.update(STREAM_ANNOTATIONS)
50 ALL_ANNOTATIONS.update(DEPRECATED_ANNOTATIONS)
51
52 # This is a mapping of old_annotation_name -> new_annotation_name.
53 # Theoretically all annotator scripts should use the new names, but it's hard
54 # to tell due to the decentralized nature of the annotator.
55 DEPRECATED_ALIASES = {
56 'BUILD_FAILED': 'STEP_FAILURE',
57 'BUILD_WARNINGS': 'STEP_WARNINGS',
58 'BUILD_EXCEPTION': 'STEP_EXCEPTION',
59 'link': 'STEP_LINK',
60 }
61
62 # A couple of the annotations have the format:
63 # @@@THING arg@@@
64 # for reasons no one knows. We only need this case until all masters have been
65 # restarted to pick up the new master-side parsing code.
66 OLD_STYLE_ANNOTATIONS = set((
67 'SEED_STEP',
68 'STEP_CURSOR',
69 ))
70
71
72 def emit(line, stream, flush_before=None):
73 if flush_before:
74 flush_before.flush()
75 print >> stream
76 # WinDOS can only handle 64kb of output to the console at a time, per process.
77 if sys.platform.startswith('win'):
78 lim = 2**15
79 while line:
80 to_print, line = line[:lim], line[lim:]
81 stream.write(to_print)
82 stream.write('\n')
83 else:
84 print >> stream, line
85 stream.flush()
86
87
88 class MetaAnnotationPrinter(type):
89 def __new__(mcs, name, bases, dct):
90 annotation_map = dct.get('ANNOTATIONS')
91 if annotation_map:
92 for key, v in annotation_map.iteritems():
93 key = key.lower()
94 dct[key] = mcs.make_printer_fn(key, v)
95 return type.__new__(mcs, name, bases, dct)
96
97 @staticmethod
98 def make_printer_fn(name, n_args):
99 """Generates a method which emits an annotation to the log stream."""
100 upname = name.upper()
101 if upname in OLD_STYLE_ANNOTATIONS:
102 assert n_args >= 1
103 fmt = '@@@%s %%s%s@@@' % (upname, '@%s' * (n_args - 1))
104 else:
105 fmt = '@@@%s%s@@@' % (upname, '@%s' * n_args)
106
107 inner_args = n_args + 1 # self counts
108 infix = '1 argument' if inner_args == 1 else ('%d arguments' % inner_args)
109 err = '%s() takes %s (%%d given)' % (name, infix)
110
111 def printer(self, *args):
112 if len(args) != n_args:
113 raise TypeError(err % (len(args) + 1))
114 self.emit(fmt % args)
115 printer.__name__ = name
116 printer.__doc__ = """Emits an annotation for %s.""" % name.upper()
117
118 return printer
119
120
121 class AnnotationPrinter(object):
122 """A derivable class which will inject annotation-printing methods into the
123 subclass.
124
125 A subclass should define a class variable ANNOTATIONS equal to a
126 dictionary of the form { '<ANNOTATION_NAME>': <# args> }. This class will
127 then inject methods whose names are the undercased version of your
128 annotation names, and which take the number of arguments specified in the
129 dictionary.
130
131 Example:
132 >>> my_annotations = { 'STEP_LOG_LINE': 2 }
133 >>> class MyObj(AnnotationPrinter):
134 ... ANNOTATIONS = my_annotations
135 ...
136 >>> o = MyObj()
137 >>> o.step_log_line('logname', 'here is a line to put in the log')
138 @@@STEP_LOG_LINE@logname@here is a line to put in the log@@@
139 >>> o.step_log_line()
140 Traceback (most recent call last):
141 File "<stdin>", line 1, in <module>
142 TypeError: step_log_line() takes exactly 3 arguments (1 given)
143 >>> o.setp_log_line.__doc__
144 "Emits an annotation for STEP_LOG_LINE."
145 >>>
146 """
147 __metaclass__ = MetaAnnotationPrinter
148
149 def __init__(self, stream, flush_before):
150 self.stream = stream
151 self.flush_before = flush_before
152
153 def emit(self, line):
154 emit(line, self.stream, self.flush_before)
155
156
157 class StepCommands(AnnotationPrinter):
158 """Class holding step commands. Intended to be subclassed."""
159 ANNOTATIONS = STEP_ANNOTATIONS
160
161 def __init__(self, stream, flush_before):
162 super(StepCommands, self).__init__(stream, flush_before)
163 self.emitted_logs = set()
164
165 def write_log_lines(self, logname, lines, perf=None):
166 if logname in self.emitted_logs:
167 raise ValueError('Log %s has been emitted multiple times.' % logname)
168 self.emitted_logs.add(logname)
169
170 logname = logname.replace('/', '&#x2f;')
171
172 for line in lines:
173 for actual_line in line.split('\n'):
174 self.step_log_line(logname, actual_line)
175
176 if perf:
177 self.step_log_end_perf(logname, perf)
178 else:
179 self.step_log_end(logname)
180
181
182 class StepControlCommands(AnnotationPrinter):
183 """Subclass holding step control commands. Intended to be subclassed.
184
185 This is subclassed out so callers in StructuredAnnotationStep can't call
186 step_started() or step_closed().
187 """
188 ANNOTATIONS = CONTROL_ANNOTATIONS
189
190
191 class StructuredAnnotationStep(StepCommands, StepControlCommands):
192 """Helper class to provide context for a step."""
193
194 def __init__(self, annotation_stream, *args, **kwargs):
195 self.annotation_stream = annotation_stream
196 super(StructuredAnnotationStep, self).__init__(*args, **kwargs)
197 self.control = StepControlCommands(self.stream, self.flush_before)
198 self.emitted_logs = set()
199
200
201 def __enter__(self):
202 return self.step_started()
203
204 def step_started(self):
205 self.control.step_started()
206 return self
207
208 def __exit__(self, exc_type, exc_value, tb):
209 self.annotation_stream.step_cursor(self.annotation_stream.current_step)
210 #TODO(martinis) combine this and step_ended
211 if exc_type:
212 self.step_exception_occured(exc_type, exc_value, tb)
213
214 self.control.step_closed()
215 self.annotation_stream.current_step = ''
216 return not exc_type
217
218 def step_exception_occured(self, exc_type, exc_value, tb):
219 trace = traceback.format_exception(exc_type, exc_value, tb)
220 trace_lines = ''.join(trace).split('\n')
221 self.write_log_lines('exception', filter(None, trace_lines))
222 self.step_exception()
223
224 def step_ended(self):
225 self.annotation_stream.step_cursor(self.annotation_stream.current_step)
226 self.control.step_closed()
227 self.annotation_stream.current_step = ''
228
229 return True
230
231
232 class StructuredAnnotationStream(AnnotationPrinter):
233 """Provides an interface to handle an annotated build.
234
235 StructuredAnnotationStream handles most of the step setup and closure calls
236 for you. All you have to do is execute your code within the steps and set any
237 failures or warnings that come up. You may optionally provide a list of steps
238 to seed before execution.
239
240 Usage:
241
242 stream = StructuredAnnotationStream()
243 with stream.step('compile') as s:
244 # do something
245 if error:
246 s.step_failure()
247 with stream.step('test') as s:
248 # do something
249 if warnings:
250 s.step_warnings()
251 """
252 ANNOTATIONS = STREAM_ANNOTATIONS
253
254 def __init__(self, stream=sys.stdout,
255 flush_before=sys.stderr,
256 seed_steps=None): # pylint: disable=W0613
257 super(StructuredAnnotationStream, self).__init__(stream=stream,
258 flush_before=flush_before)
259 self.current_step = ''
260
261 def step(self, name):
262 """Provide a context with which to execute a step."""
263 if self.current_step:
264 raise Exception('Can\'t start step %s while in step %s.' % (
265 name, self.current_step))
266
267 self.seed_step(name)
268 self.step_cursor(name)
269 self.current_step = name
270 return StructuredAnnotationStep(self, stream=self.stream,
271 flush_before=self.flush_before)
272
273
274 def MatchAnnotation(line, callback_implementor):
275 """Call back into |callback_implementor| if line contains an annotation.
276
277 Args:
278 line (str) - The line to analyze
279 callback_implementor (object) - An object which contains methods
280 corresponding to all of the annotations in the |ALL_ANNOTATIONS|
281 dictionary. For example, it should contain a method STEP_SUMMARY_TEXT
282 taking a single argument.
283
284 Parsing method:
285 * if line doesn't match /^@@@.*@@@$/, return without calling back
286 * Look for the first '@' or ' '
287 """
288 if not (line.startswith('@@@') and line.endswith('@@@') and len(line) > 6):
289 return
290 line = line[3:-3]
291
292 # look until the first @ or ' '
293 idx = min((x for x in (line.find('@'), line.find(' '), len(line)) if x > 0))
294 cmd_text = line[:idx]
295 cmd = DEPRECATED_ALIASES.get(cmd_text, cmd_text)
296
297 field_count = ALL_ANNOTATIONS.get(cmd)
298 if field_count is None:
299 raise Exception('Unrecognized annotator command "%s"' % cmd_text)
300
301 if field_count:
302 if idx == len(line):
303 raise Exception('Annotator command "%s" expects %d args, got 0.'
304 % (cmd_text, field_count))
305
306 line = line[idx+1:]
307
308 args = line.split('@', field_count-1)
309 if len(args) != field_count:
310 raise Exception('Annotator command "%s" expects %d args, got %d.'
311 % (cmd_text, field_count, len(args)))
312 else:
313 line = line[len(cmd_text):]
314 if line:
315 raise Exception('Annotator command "%s" expects no args, got cruft "%s".'
316 % (cmd_text, line))
317 args = []
318
319 fn = getattr(callback_implementor, cmd, None)
320 if fn is None:
321 raise Exception('"%s" does not implement "%s"'
322 % (callback_implementor, cmd))
323
324 fn(*args)
325
326
327 def print_step(step, env, stream):
328 """Prints the step command and relevant metadata.
329
330 Intended to be similar to the information that Buildbot prints at the
331 beginning of each non-annotator step.
332 """
333 step_info_lines = []
334 step_info_lines.append(' '.join(step['cmd']))
335 step_info_lines.append('in dir %s:' % (step['cwd'] or os.getcwd()))
336 for key, value in sorted(step.items()):
337 if value is not None:
338 if callable(value):
339 # This prevents functions from showing up as:
340 # '<function foo at 0x7f523ec7a410>'
341 # which is tricky to test.
342 value = value.__name__+'(...)'
343 step_info_lines.append(' %s: %s' % (key, value))
344 step_info_lines.append('full environment:')
345 for key, value in sorted(env.items()):
346 step_info_lines.append(' %s: %s' % (key, value))
347 step_info_lines.append('')
348 stream.emit('\n'.join(step_info_lines))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698