OLD | NEW |
| (Empty) |
1 #!/usr/bin/python | |
2 # | |
3 # Copyright (c) 2010 The Chromium OS Authors. All rights reserved. | |
4 # Use of this source code is governed by a BSD-style license that can be | |
5 # found in the LICENSE file. | |
6 | |
7 | |
8 # DESCRIPTION : | |
9 # | |
10 # This UI is intended to be used by the factory autotest suite to | |
11 # provide factory operators feedback on test status and control over | |
12 # execution order. | |
13 # | |
14 # In short, the UI is composed of a 'console' panel on the bottom of | |
15 # the screen which displays the autotest log, and there is also a | |
16 # 'test list' panel on the right hand side of the screen. The | |
17 # majority of the screen is dedicated to tests, which are executed in | |
18 # seperate processes, but instructed to display their own UIs in this | |
19 # dedicated area whenever possible. Tests in the test list are | |
20 # executed in order by default, but can be activated on demand via | |
21 # associated keyboard shortcuts (triggers). As tests are run, their | |
22 # status is color-indicated to the operator -- greyed out means | |
23 # untested, yellow means active, green passed and red failed. | |
24 | |
25 | |
26 import gobject | |
27 import gtk | |
28 import os | |
29 import pango | |
30 import subprocess | |
31 import sys | |
32 import time | |
33 | |
34 | |
35 def XXX_log(s): | |
36 print >> sys.stderr, '--- XXX : ' + s | |
37 | |
38 _ACTIVE = 'ACTIVE' | |
39 _PASSED = 'PASS' | |
40 _FAILED = 'FAIL' | |
41 _UNTESTED = 'UNTESTED' | |
42 | |
43 _LABEL_COLORS = { | |
44 _ACTIVE: gtk.gdk.color_parse('light goldenrod'), | |
45 _PASSED: gtk.gdk.color_parse('pale green'), | |
46 _FAILED: gtk.gdk.color_parse('tomato'), | |
47 _UNTESTED: gtk.gdk.color_parse('dark slate grey')} | |
48 | |
49 _LABEL_EN_SIZE = (160, 35) | |
50 _LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16') | |
51 _LABEL_ZW_SIZE = (70, 35) | |
52 _LABEL_ZW_FONT = pango.FontDescription('normal 12') | |
53 _LABEL_T_SIZE = (30, 35) | |
54 _LABEL_T_FONT = pango.FontDescription('courier new italic ultra-condensed 10') | |
55 _LABEL_UNTESTED_FG = gtk.gdk.color_parse('grey40') | |
56 _LABEL_TROUGH_COLOR = gtk.gdk.color_parse('grey20') | |
57 _LABEL_STATUS_SIZE = (140, 30) | |
58 _LABEL_STATUS_FONT = pango.FontDescription( | |
59 'courier new bold extra-condensed 16') | |
60 _SEP_COLOR = gtk.gdk.color_parse('grey50') | |
61 _BLACK = gtk.gdk.color_parse('black') | |
62 _LIGHT_GREEN = gtk.gdk.color_parse('light green') | |
63 _OTHER_LABEL_FONT = pango.FontDescription('courier new condensed 20') | |
64 | |
65 class console_proc: | |
66 '''Display a progress log. Implemented by launching an borderless | |
67 xterm at a strategic location, and running tail against the log.''' | |
68 | |
69 def __init__(self, allocation, log_file_path): | |
70 xterm_coords = '135x14+%d+%d' % (allocation.x, allocation.y) | |
71 XXX_log('xterm_coords = %s' % xterm_coords) | |
72 xterm_cmd = ('xterm --geometry %s -bw 0 -e ' % xterm_coords + | |
73 'tail -f %s' % log_file_path) | |
74 self._proc = subprocess.Popen(xterm_cmd.split()) | |
75 | |
76 def __del__(self): | |
77 XXX_log('console_proc __del__') | |
78 self._proc.kill() | |
79 | |
80 | |
81 # Routines to communicate with the autotest control file, using python | |
82 # expressions. The stdin_callback assures notification for any new | |
83 # messages. | |
84 | |
85 def stdin_callback(s, c): | |
86 XXX_log('stdin_callback, quitting gtk main') | |
87 gtk.main_quit() | |
88 return True | |
89 | |
90 def control_recv(): | |
91 return eval(sys.stdin.readline().rstrip()) | |
92 | |
93 def control_send(x): | |
94 print repr(x) | |
95 sys.stdout.flush() | |
96 | |
97 def control_send_target_test_update(test): | |
98 XXX_log('ui send_target_test_update %s.%s_%s' % | |
99 (test.formal_name, test.tag_prefix, test.count)) | |
100 control_send((test.formal_name, test.tag_prefix, test.count)) | |
101 | |
102 | |
103 # Capture keyboard events here for debugging -- under normal | |
104 # circumstances, all keyboard events should be captured by executing | |
105 # tests, and hence this should not be called. | |
106 | |
107 def handle_key_release_event(_, event): | |
108 XXX_log('base ui key event (%s)' % event.keyval) | |
109 return True | |
110 | |
111 | |
112 class test_label_box(gtk.EventBox): | |
113 | |
114 def __init__(self, test): | |
115 gtk.EventBox.__init__(self) | |
116 label_en = gtk.Label(test.label_en) | |
117 label_en.set_size_request(*_LABEL_EN_SIZE) | |
118 label_en.modify_font(_LABEL_EN_FONT) | |
119 label_en.set_alignment(0.8, 0.5) | |
120 label_en.modify_fg(gtk.STATE_NORMAL, _LABEL_UNTESTED_FG) | |
121 label_zw = gtk.Label(test.label_zw) | |
122 label_zw.set_size_request(*_LABEL_ZW_SIZE) | |
123 label_zw.modify_font(_LABEL_ZW_FONT) | |
124 label_zw.set_alignment(0.2, 0.5) | |
125 label_zw.modify_fg(gtk.STATE_NORMAL, _LABEL_UNTESTED_FG) | |
126 label_t = gtk.Label('C-' + test.trigger) | |
127 label_t.set_size_request(*_LABEL_T_SIZE) | |
128 label_t.modify_font(_LABEL_T_FONT) | |
129 label_t.set_alignment(0.5, 0.5) | |
130 label_t.modify_fg(gtk.STATE_NORMAL, _BLACK) | |
131 hbox = gtk.HBox() | |
132 hbox.pack_start(label_en, False, False) | |
133 hbox.pack_start(label_zw, False, False) | |
134 hbox.pack_start(label_t, False, False) | |
135 self.add(hbox) | |
136 self.label_list = [label_en, label_zw] | |
137 | |
138 def update_status(self, status): | |
139 if status != _UNTESTED: | |
140 self.modify_fg(gtk.STATE_NORMAL, _BLACK) | |
141 for label in self.label_list: | |
142 label.modify_fg(gtk.STATE_NORMAL, _BLACK) | |
143 self.modify_bg(gtk.STATE_NORMAL, _LABEL_COLORS[status]) | |
144 self.queue_draw() | |
145 | |
146 | |
147 class subtest_label_box(gtk.EventBox): | |
148 | |
149 def __init__(self, test): | |
150 gtk.EventBox.__init__(self) | |
151 self.modify_bg(gtk.STATE_NORMAL, _BLACK) | |
152 label_status = gtk.Label(_UNTESTED) | |
153 label_status.set_size_request(*_LABEL_STATUS_SIZE) | |
154 label_status.set_alignment(0, 0.5) | |
155 label_status.modify_font(_LABEL_STATUS_FONT) | |
156 label_status.modify_fg(gtk.STATE_NORMAL, _LABEL_UNTESTED_FG) | |
157 label_en = gtk.Label(test.label_en) | |
158 label_en.set_alignment(1, 0.5) | |
159 label_en.modify_font(_LABEL_EN_FONT) | |
160 label_en.modify_fg(gtk.STATE_NORMAL, _LIGHT_GREEN) | |
161 label_zw = gtk.Label(test.label_zw) | |
162 label_zw.set_alignment(1, 0.5) | |
163 label_zw.modify_font(_LABEL_ZW_FONT) | |
164 label_zw.modify_fg(gtk.STATE_NORMAL, _LIGHT_GREEN) | |
165 label_sep = gtk.Label(' : ') | |
166 label_sep.set_alignment(0.5, 0.5) | |
167 label_sep.modify_font(_LABEL_EN_FONT) | |
168 label_sep.modify_fg(gtk.STATE_NORMAL, _LIGHT_GREEN) | |
169 hbox = gtk.HBox() | |
170 hbox.pack_end(label_status, False, False) | |
171 hbox.pack_end(label_sep, False, False) | |
172 hbox.pack_end(label_zw, False, False) | |
173 hbox.pack_end(label_en, False, False) | |
174 self.add(hbox) | |
175 self.label_status = label_status | |
176 | |
177 def update_status(self, status): | |
178 if status != _UNTESTED: | |
179 self.label_status.set_text(status) | |
180 self.label_status.modify_fg(gtk.STATE_NORMAL, _LABEL_COLORS[status]) | |
181 self.queue_draw() | |
182 | |
183 | |
184 class status_map(): | |
185 | |
186 def __init__(self): | |
187 self.status_dict = {} | |
188 | |
189 def index(self, formal_name, tag_prefix): | |
190 return '%s.%s' % (formal_name, tag_prefix) | |
191 | |
192 def lookup(self, formal_name, tag_prefix): | |
193 return self.status_dict.setdefault( | |
194 self.index(formal_name, tag_prefix), | |
195 (_UNTESTED, 0)) | |
196 | |
197 def update(self, formal_name, tag_prefix, status, count): | |
198 _, existing_count = self.lookup(formal_name, tag_prefix) | |
199 if count > existing_count: | |
200 index = self.index(formal_name, tag_prefix) | |
201 self.status_dict[index] = (status, count) | |
202 | |
203 def get_subtest_status(self, test): | |
204 map(self.set_test_status, test.automated_seq) | |
205 sub_status_set = set(st.status for st in test.automated_seq) | |
206 min_count = min([st.count for st in test.automated_seq]) | |
207 max_count = max([st.count for st in test.automated_seq]) | |
208 if len(sub_status_set) == 1: | |
209 return (sub_status_set.pop(), max_count) | |
210 if test.count > min_count: | |
211 return (_ACTIVE, max_count) | |
212 return (_FAILED, max_count) | |
213 | |
214 def set_test_status(self, test): | |
215 status, count = ( | |
216 test.automated_seq | |
217 and self.get_subtest_status(test) | |
218 or self.lookup(test.formal_name, test.tag_prefix)) | |
219 status = test.count > count and _ACTIVE or status | |
220 max_count = max(test.count, count) | |
221 if test.status != status or test.count != max_count: | |
222 XXX_log('status change for %s : %s/%s -> %s/%s' % | |
223 (self.index(test.formal_name, test.tag_prefix), | |
224 test.count, test.status, max_count, status)) | |
225 test.status = status | |
226 test.count = max_count | |
227 test.label_box.update_status(status) | |
228 | |
229 | |
230 def refresh_test_status(status_file_path, test_list): | |
231 smap = status_map() | |
232 with open(status_file_path) as file: | |
233 for line in file: | |
234 columns = line.split('\t') | |
235 if len(columns) >= 8 and not columns[0] and not columns[1]: | |
236 status = columns[2] == 'GOOD' and _PASSED or _FAILED | |
237 formal_name, _, tag = columns[3].rpartition('.') | |
238 tag_prefix, _, count = tag.rpartition('_') | |
239 count = int(count) | |
240 smap.update(formal_name, tag_prefix, status, count) | |
241 map(smap.set_test_status, test_list) | |
242 | |
243 | |
244 def set_active_test(test): | |
245 test.count += 1 | |
246 test.label_box.update_status(_ACTIVE) | |
247 control_send_target_test_update(test) | |
248 | |
249 | |
250 def make_hsep(width=1): | |
251 frame = gtk.EventBox() | |
252 frame.set_size_request(-1, width) | |
253 frame.modify_bg(gtk.STATE_NORMAL, _SEP_COLOR) | |
254 return frame | |
255 | |
256 | |
257 def make_vsep(width=1): | |
258 frame = gtk.EventBox() | |
259 frame.set_size_request(width, -1) | |
260 frame.modify_bg(gtk.STATE_NORMAL, _SEP_COLOR) | |
261 return frame | |
262 | |
263 | |
264 def make_notest_label(): | |
265 label = gtk.Label('no active test') | |
266 label.modify_font(_OTHER_LABEL_FONT) | |
267 label.set_alignment(0.5, 0.5) | |
268 label.modify_fg(gtk.STATE_NORMAL, _LIGHT_GREEN) | |
269 box = gtk.EventBox() | |
270 box.modify_bg(gtk.STATE_NORMAL, _BLACK) | |
271 box.add(label) | |
272 return box | |
273 | |
274 | |
275 def make_automated_seq_widget(as_test): | |
276 vbox = gtk.VBox() | |
277 vbox.set_spacing(0) | |
278 map(lambda st: vbox.pack_start(st.label_box, False, False), | |
279 as_test.automated_seq) | |
280 return vbox | |
281 | |
282 | |
283 def main(): | |
284 window = gtk.Window(gtk.WINDOW_TOPLEVEL) | |
285 window.connect('destroy', lambda _: gtk.main_quit()) | |
286 window.modify_bg(gtk.STATE_NORMAL, _BLACK) | |
287 | |
288 screen = window.get_screen() | |
289 screen_size = (screen.get_width(), screen.get_height()) | |
290 window.set_size_request(*screen_size) | |
291 | |
292 label_trough = gtk.VBox() | |
293 label_trough.set_spacing(0) | |
294 | |
295 rhs_box = gtk.EventBox() | |
296 rhs_box.modify_bg(gtk.STATE_NORMAL, _LABEL_TROUGH_COLOR) | |
297 rhs_box.add(label_trough) | |
298 | |
299 console_box = gtk.EventBox() | |
300 console_box.set_size_request(-1, 180) | |
301 console_box.modify_bg(gtk.STATE_NORMAL, _BLACK) | |
302 | |
303 notest_label = make_notest_label() | |
304 | |
305 test_widget_box = gtk.Alignment(xalign=0.5, yalign=0.5) | |
306 test_widget_box.set_size_request(-1, -1) | |
307 test_widget_box.add(notest_label) | |
308 | |
309 lhs_box = gtk.VBox() | |
310 lhs_box.pack_end(console_box, False, False) | |
311 lhs_box.pack_start(test_widget_box) | |
312 lhs_box.pack_start(make_hsep(3), False, False) | |
313 | |
314 base_box = gtk.HBox() | |
315 base_box.pack_end(rhs_box, False, False) | |
316 base_box.pack_end(make_vsep(3), False, False) | |
317 base_box.pack_start(lhs_box) | |
318 | |
319 window.connect('key-release-event', handle_key_release_event) | |
320 window.add_events(gtk.gdk.KEY_RELEASE_MASK) | |
321 | |
322 # On startup, get general configuration data from the autotest | |
323 # control program, specifically the list of tests to run (in | |
324 # order) and some filenames. | |
325 XXX_log('pulling control info') | |
326 test_list = control_recv() | |
327 status_file_path = control_recv() | |
328 log_file_path = control_recv() | |
329 | |
330 for test in test_list: | |
331 test.status = None | |
332 test.count = 0 | |
333 test.tag_prefix = test.trigger | |
334 test.label_box = test_label_box(test) | |
335 for subtest in test.automated_seq: | |
336 subtest.status = None | |
337 subtest.count = 0 | |
338 subtest.tag_prefix = test.formal_name | |
339 subtest.label_box = subtest_label_box(subtest) | |
340 label_trough.pack_start(test.label_box, False, False) | |
341 label_trough.pack_start(make_hsep(), False, False) | |
342 | |
343 window.add(base_box) | |
344 window.show_all() | |
345 | |
346 test_widget_allocation = test_widget_box.get_allocation() | |
347 test_widget_size = (test_widget_allocation.width, | |
348 test_widget_allocation.height) | |
349 XXX_log('test_widget_size = %s' % repr(test_widget_size)) | |
350 control_send(test_widget_size) | |
351 | |
352 trigger_dict = dict((test.trigger, test) for test in test_list) | |
353 | |
354 refresh_test_status(status_file_path, test_list) | |
355 remaining_tests_queue = [x for x in reversed(test_list) | |
356 if x.status == _UNTESTED] | |
357 XXX_log('remaining_tests_queue = %s' % | |
358 repr([x.label_en for x in remaining_tests_queue])) | |
359 | |
360 active_test = None | |
361 | |
362 gobject.io_add_watch(sys.stdin, gobject.IO_IN, stdin_callback) | |
363 | |
364 console = console_proc(console_box.get_allocation(), log_file_path) | |
365 | |
366 XXX_log('finished ui setup') | |
367 | |
368 # Test selection is driven either by triggers or by the | |
369 # remaining_tests_queue. If a trigger was seen, explicitly run | |
370 # the corresponding test. Otherwise choose the next test from the | |
371 # queue. Tests are removed from the queue as they are run, | |
372 # regarless of the outcome. Tests that are interrupted by trigger | |
373 # are treated as having failed. | |
374 # | |
375 # Iterations in the main loop here are driven by data availability | |
376 # on stdin, which is used to communicate with the autotest control | |
377 # program. On each step through the loop, a trigger is received | |
378 # (possibly None) to indicate how the next test should be selected. | |
379 | |
380 while remaining_tests_queue: | |
381 command, arg = control_recv() | |
382 XXX_log('ui received command %s(%s)' % (command, arg)) | |
383 if command == 'switch_to': | |
384 active_test = trigger_dict.get(arg, None) | |
385 if active_test in remaining_tests_queue: | |
386 remaining_tests_queue.remove(active_test) | |
387 set_active_test(active_test) | |
388 elif command == 'next_test': | |
389 active_test = remaining_tests_queue.pop() | |
390 set_active_test(active_test) | |
391 else: | |
392 XXX_log('ui command unknown, exiting...') | |
393 break | |
394 if active_test.automated_seq: | |
395 XXX_log('ui starting automated_seq') | |
396 subtest_queue = [x for x in reversed(active_test.automated_seq)] | |
397 test_widget_box.remove(notest_label) | |
398 as_widget = make_automated_seq_widget(active_test) | |
399 test_widget_box.add(as_widget) | |
400 window.show_all() | |
401 command = None | |
402 while command != 'quit_automated_seq': | |
403 active_subtest = subtest_queue.pop() | |
404 active_subtest.label_box.update_status(_ACTIVE) | |
405 gtk.main() | |
406 command = control_recv() | |
407 XXX_log('ui automated_seq step (%s)' % command) | |
408 refresh_test_status(status_file_path, test_list) | |
409 test_widget_box.queue_draw() | |
410 test_widget_box.remove(as_widget) | |
411 test_widget_box.add(notest_label) | |
412 window.show_all() | |
413 XXX_log('ui exiting automated_seq') | |
414 else: | |
415 gtk.main() | |
416 refresh_test_status(status_file_path, test_list) | |
417 | |
418 # Tell the control process we are done. | |
419 control_send((None, 0)) | |
420 | |
421 XXX_log('exiting ui') | |
422 | |
423 if __name__ == '__main__': | |
424 | |
425 # In global scope, get the test_data class description from the | |
426 # control program -- this allows a convenient single point of | |
427 # definition for this class. | |
428 test_data_class_def = control_recv() | |
429 exec(test_data_class_def) | |
430 | |
431 main() | |
OLD | NEW |