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