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 |
| 39 _LABEL_COLORS = { |
| 40 'active': gtk.gdk.color_parse('light goldenrod'), |
| 41 'passed': gtk.gdk.color_parse('pale green'), |
| 42 'failed': gtk.gdk.color_parse('tomato'), |
| 43 'untested': gtk.gdk.color_parse('dark slate grey')} |
| 44 |
| 45 _LABEL_EN_SIZE = (160, 35) |
| 46 _LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16') |
| 47 _LABEL_ZW_SIZE = (70, 35) |
| 48 _LABEL_ZW_FONT = pango.FontDescription('normal 12') |
| 49 _LABEL_T_SIZE = (30, 35) |
| 50 _LABEL_T_FONT = pango.FontDescription('courier new italic ultra-condensed 10') |
| 51 _LABEL_UNTESTED_FG = gtk.gdk.color_parse('grey40') |
| 52 _LABEL_TROUGH_COLOR = gtk.gdk.color_parse('grey20') |
| 53 _SEP_COLOR = gtk.gdk.color_parse('grey50') |
| 54 _BLACK = gtk.gdk.color_parse('black') |
| 55 _LIGHT_GREEN = gtk.gdk.color_parse('light green') |
| 56 |
| 57 |
| 58 class console_proc: |
| 59 '''Display a progress log. Implemented by launching an borderless |
| 60 xterm at a strategic location, and running tail against the log.''' |
| 61 |
| 62 def __init__(self, allocation, log_file_path): |
| 63 xterm_coords = '135x14+%d+%d' % (allocation.x, allocation.y) |
| 64 XXX_log('xterm_coords = %s' % xterm_coords) |
| 65 xterm_cmd = ('xterm --geometry %s -bw 0 -e ' % xterm_coords + |
| 66 'tail -f %s' % log_file_path) |
| 67 self._proc = subprocess.Popen(xterm_cmd.split()) |
| 68 |
| 69 def __del__(self): |
| 70 XXX_log('console_proc __del__') |
| 71 self._proc.kill() |
| 72 |
| 73 |
| 74 # Routines to communicate with the autotest control file, using python |
| 75 # expressions. The stdin_callback assures notification for any new |
| 76 # messages. |
| 77 |
| 78 def stdin_callback(s, c): |
| 79 XXX_log('stdin_callback, quitting gtk main') |
| 80 gtk.main_quit() |
| 81 return True |
| 82 |
| 83 def control_recv(): |
| 84 return eval(sys.stdin.readline().rstrip()) |
| 85 |
| 86 def control_send(x): |
| 87 print repr(x) |
| 88 sys.stdout.flush() |
| 89 |
| 90 |
| 91 # Capture keyboard events here for debugging -- under normal |
| 92 # circumstances, all keyboard events should be captured by executing |
| 93 # tests, and hence this should not be called. |
| 94 |
| 95 def handle_key_release_event(_, event): |
| 96 XXX_log('base ui key event (%s)' % event.keyval) |
| 97 return True |
| 98 |
| 99 |
| 100 def update_label_status(test, status): |
| 101 if status != 'untested': |
| 102 test.label_box.modify_fg(gtk.STATE_NORMAL, _BLACK) |
| 103 for label in test.label_list: |
| 104 label.modify_fg(gtk.STATE_NORMAL, _BLACK) |
| 105 test.label_box.modify_bg(gtk.STATE_NORMAL, _LABEL_COLORS[status]) |
| 106 test.label_box.queue_draw() |
| 107 |
| 108 |
| 109 def refresh_test_status(status_file_path, test_list): |
| 110 result_dict = {} |
| 111 with open(status_file_path) as file: |
| 112 for line in file: |
| 113 columns = line.split('\t') |
| 114 if len(columns) >= 8 and not columns[0] and not columns[1]: |
| 115 result_state = columns[2] |
| 116 full_name = columns[3] |
| 117 result_dict[full_name] = result_state |
| 118 for test in test_list: |
| 119 full_name = '%s.%d' % (test.formal_name, test.count) |
| 120 result_state = result_dict.get(full_name, None) |
| 121 if result_state is None: |
| 122 status = 'untested' |
| 123 elif result_state == 'GOOD': |
| 124 status = 'passed' |
| 125 else: |
| 126 status = 'failed' |
| 127 if test.status != status: |
| 128 XXX_log('status change for %s : %s -> %s' % |
| 129 (test.label_en, test.status, status)) |
| 130 test.status = status |
| 131 update_label_status(test, status) |
| 132 |
| 133 |
| 134 def select_active_test(test_list, remaining_tests_queue, |
| 135 test_counters, trigger): |
| 136 active_test = None |
| 137 if trigger is not None: |
| 138 trigger_dict = dict((test.trigger, test) for test in test_list) |
| 139 active_test = trigger_dict.get(trigger, None) |
| 140 if active_test in remaining_tests_queue: |
| 141 remaining_tests_queue.remove(active_test) |
| 142 if active_test is None: |
| 143 active_test = remaining_tests_queue.pop() |
| 144 count = test_counters[active_test.formal_name] |
| 145 count += 1 |
| 146 active_test.count = count |
| 147 test_counters[active_test.formal_name] = count |
| 148 update_label_status(active_test, 'active') |
| 149 XXX_log('select_active_test %s.%d' % |
| 150 (active_test.formal_name, active_test.count)) |
| 151 return (active_test.label_en, active_test.count) |
| 152 |
| 153 |
| 154 def make_test_label(test): |
| 155 label_en = gtk.Label(test.label_en) |
| 156 label_en.set_size_request(*_LABEL_EN_SIZE) |
| 157 label_en.modify_font(_LABEL_EN_FONT) |
| 158 label_en.set_alignment(0.8, 0.5) |
| 159 label_en.modify_fg(gtk.STATE_NORMAL, _LABEL_UNTESTED_FG) |
| 160 label_zw = gtk.Label(test.label_zw) |
| 161 label_zw.set_size_request(*_LABEL_ZW_SIZE) |
| 162 label_zw.modify_font(_LABEL_ZW_FONT) |
| 163 label_zw.set_alignment(0.2, 0.5) |
| 164 label_zw.modify_fg(gtk.STATE_NORMAL, _LABEL_UNTESTED_FG) |
| 165 label_t = gtk.Label('C-' + test.trigger) |
| 166 label_t.set_size_request(*_LABEL_T_SIZE) |
| 167 label_t.modify_font(_LABEL_T_FONT) |
| 168 label_t.set_alignment(0.5, 0.5) |
| 169 label_t.modify_fg(gtk.STATE_NORMAL, _BLACK) |
| 170 hbox = gtk.HBox() |
| 171 hbox.pack_start(label_en, False, False) |
| 172 hbox.pack_start(label_zw, False, False) |
| 173 hbox.pack_start(label_t, False, False) |
| 174 label_box = gtk.EventBox() |
| 175 label_box.add(hbox) |
| 176 test.label_box = label_box |
| 177 test.label_list = [label_en, label_zw] |
| 178 return label_box |
| 179 |
| 180 |
| 181 def make_hsep(width=1): |
| 182 frame = gtk.EventBox() |
| 183 frame.set_size_request(-1, width) |
| 184 frame.modify_bg(gtk.STATE_NORMAL, _SEP_COLOR) |
| 185 return frame |
| 186 |
| 187 |
| 188 def make_vsep(width=1): |
| 189 frame = gtk.EventBox() |
| 190 frame.set_size_request(width, -1) |
| 191 frame.modify_bg(gtk.STATE_NORMAL, _SEP_COLOR) |
| 192 return frame |
| 193 |
| 194 |
| 195 def make_test_widget_box(): |
| 196 label = gtk.Label('no active test') |
| 197 font = pango.FontDescription('courier new condensed 20') |
| 198 label.modify_font(font) |
| 199 label.set_alignment(0.5, 0.5) |
| 200 label.modify_fg(gtk.STATE_NORMAL, _LIGHT_GREEN) |
| 201 box = gtk.EventBox() |
| 202 box.modify_bg(gtk.STATE_NORMAL, _BLACK) |
| 203 box.add(label) |
| 204 align = gtk.Alignment(xalign=0.5, yalign=0.5) |
| 205 align.set_size_request(-1, -1) |
| 206 align.add(box) |
| 207 return align |
| 208 |
| 209 |
| 210 def main(): |
| 211 window = gtk.Window(gtk.WINDOW_TOPLEVEL) |
| 212 window.connect('destroy', lambda _: gtk.main_quit()) |
| 213 window.modify_bg(gtk.STATE_NORMAL, _BLACK) |
| 214 |
| 215 screen = window.get_screen() |
| 216 screen_size = (screen.get_width(), screen.get_height()) |
| 217 window.set_size_request(*screen_size) |
| 218 |
| 219 label_trough = gtk.VBox() |
| 220 label_trough.set_spacing(0) |
| 221 |
| 222 rhs_box = gtk.EventBox() |
| 223 rhs_box.modify_bg(gtk.STATE_NORMAL, _LABEL_TROUGH_COLOR) |
| 224 rhs_box.add(label_trough) |
| 225 |
| 226 console_box = gtk.EventBox() |
| 227 console_box.set_size_request(-1, 180) |
| 228 console_box.modify_bg(gtk.STATE_NORMAL, _BLACK) |
| 229 |
| 230 test_widget_box = make_test_widget_box() |
| 231 |
| 232 lhs_box = gtk.VBox() |
| 233 lhs_box.pack_end(console_box, False, False) |
| 234 lhs_box.pack_start(test_widget_box) |
| 235 lhs_box.pack_start(make_hsep(3), False, False) |
| 236 |
| 237 base_box = gtk.HBox() |
| 238 base_box.pack_end(rhs_box, False, False) |
| 239 base_box.pack_end(make_vsep(3), False, False) |
| 240 base_box.pack_start(lhs_box) |
| 241 |
| 242 window.connect('key-release-event', handle_key_release_event) |
| 243 window.add_events(gtk.gdk.KEY_RELEASE_MASK) |
| 244 |
| 245 # On startup, get general configuration data from the autotest |
| 246 # control program, specifically the list of tests to run (in |
| 247 # order) and some filenames. |
| 248 XXX_log('pulling control info') |
| 249 test_list = control_recv() |
| 250 status_file_path = control_recv() |
| 251 log_file_path = control_recv() |
| 252 |
| 253 for test in test_list: |
| 254 test.status = None |
| 255 label = make_test_label(test) |
| 256 label_trough.pack_start(label, False, False) |
| 257 label_trough.pack_start(make_hsep(), False, False) |
| 258 |
| 259 window.add(base_box) |
| 260 window.show_all() |
| 261 |
| 262 test_widget_allocation = test_widget_box.get_allocation() |
| 263 test_widget_size = (test_widget_allocation.width, |
| 264 test_widget_allocation.height) |
| 265 XXX_log('test_widget_size = %s' % repr(test_widget_size)) |
| 266 control_send(test_widget_size) |
| 267 |
| 268 # Use a common datastructure for counters to allow multiple tests |
| 269 # to share the same formal name. |
| 270 test_counters = dict((test.formal_name, 0) for test in test_list) |
| 271 for test in test_list: |
| 272 test.count = 0 |
| 273 |
| 274 refresh_test_status(status_file_path, test_list) |
| 275 remaining_tests_queue = [x for x in reversed(test_list) |
| 276 if test.status != 'passed'] |
| 277 |
| 278 gobject.io_add_watch(sys.stdin, gobject.IO_IN, stdin_callback) |
| 279 |
| 280 console = console_proc(console_box.get_allocation(), log_file_path) |
| 281 |
| 282 XXX_log('finished ui setup') |
| 283 |
| 284 # Test selection is driven either by triggers or by the |
| 285 # remaining_tests_queue. If a trigger was seen, explicitly run |
| 286 # the corresponding test. Otherwise choose the next test from the |
| 287 # queue. Tests are removed from the queue as they are run, |
| 288 # regarless of the outcome. Tests that are interrupted by trigger |
| 289 # are treated as having failed. |
| 290 # |
| 291 # Iterations in the main loop here are driven by data availability |
| 292 # on stdin, which is used to communicate with the autotest control |
| 293 # program. On each step through the loop, a trigger is received |
| 294 # (possibly None) to indicate how the next test should be selected. |
| 295 |
| 296 while remaining_tests_queue: |
| 297 trigger = control_recv() |
| 298 XXX_log('ui received trigger (%s)' % trigger) |
| 299 active_test_name, count = select_active_test( |
| 300 test_list, remaining_tests_queue, |
| 301 test_counters, trigger) |
| 302 control_send((active_test_name, count)) |
| 303 gtk.main() |
| 304 refresh_test_status(status_file_path, test_list) |
| 305 |
| 306 control_send((None, 0)) |
| 307 |
| 308 XXX_log('exiting ui') |
| 309 |
| 310 if __name__ == '__main__': |
| 311 |
| 312 # In global scope, get the test_data class description from the |
| 313 # control program -- this allows a convenient single point of |
| 314 # definition for this class. |
| 315 test_data_class_def = control_recv() |
| 316 exec(test_data_class_def) |
| 317 |
| 318 main() |
OLD | NEW |