OLD | NEW |
| (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('/', '/') | |
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)) | |
OLD | NEW |