| 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 |