blob: 28a12c7e4190aca39425b6f183e3b2afd01f8385 [file] [log] [blame]
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001#!/usr/bin/python -u
Hung-Te Linf2f78f72012-02-08 19:27:11 +08002# -*- coding: utf-8 -*-
3#
4# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
5# Use of this source code is governed by a BSD-style license that can be
6# found in the LICENSE file.
7
8
9# DESCRIPTION :
10#
11# This library provides convenience routines to launch factory tests.
12# This includes support for drawing the test widget in a window at the
13# proper location, grabbing control of the mouse, and making the mouse
14# cursor disappear.
Hung-Te Lin6bb48552012-02-09 14:37:43 +080015#
16# This UI is intended to be used by the factory autotest suite to
17# provide factory operators feedback on test status and control over
18# execution order.
19#
20# In short, the UI is composed of a 'console' panel on the bottom of
21# the screen which displays the autotest log, and there is also a
22# 'test list' panel on the right hand side of the screen. The
23# majority of the screen is dedicated to tests, which are executed in
24# seperate processes, but instructed to display their own UIs in this
25# dedicated area whenever possible. Tests in the test list are
26# executed in order by default, but can be activated on demand via
27# associated keyboard shortcuts. As tests are run, their status is
28# color-indicated to the operator -- greyed out means untested, yellow
29# means active, green passed and red failed.
Hung-Te Linf2f78f72012-02-08 19:27:11 +080030
Hung-Te Lin6bb48552012-02-09 14:37:43 +080031import logging
32import os
33import re
34import subprocess
35import sys
Hung-Te Linf2f78f72012-02-08 19:27:11 +080036from itertools import izip, product
37
Hung-Te Lin6bb48552012-02-09 14:37:43 +080038# GTK and X modules
39import gobject
40import gtk
41import pango
42
43# Factory and autotest modules
Hung-Te Linf2f78f72012-02-08 19:27:11 +080044import factory_common
45from autotest_lib.client.cros import factory
46from autotest_lib.client.cros.factory import TestState
47from autotest_lib.client.cros.factory.event import Event, EventClient
48
Hung-Te Lin6bb48552012-02-09 14:37:43 +080049
Hung-Te Linf2f78f72012-02-08 19:27:11 +080050# For compatibility with tests before TestState existed
51ACTIVE = TestState.ACTIVE
52PASSED = TestState.PASSED
53FAILED = TestState.FAILED
54UNTESTED = TestState.UNTESTED
55
Hung-Te Lin6bb48552012-02-09 14:37:43 +080056# Color definition
Hung-Te Linf2f78f72012-02-08 19:27:11 +080057BLACK = gtk.gdk.Color()
58RED = gtk.gdk.Color(0xFFFF, 0, 0)
59GREEN = gtk.gdk.Color(0, 0xFFFF, 0)
60BLUE = gtk.gdk.Color(0, 0, 0xFFFF)
61WHITE = gtk.gdk.Color(0xFFFF, 0xFFFF, 0xFFFF)
Hung-Te Linf2f78f72012-02-08 19:27:11 +080062LIGHT_GREEN = gtk.gdk.color_parse('light green')
Hung-Te Linf2f78f72012-02-08 19:27:11 +080063SEP_COLOR = gtk.gdk.color_parse('grey50')
64
65RGBA_GREEN_OVERLAY = (0, 0.5, 0, 0.6)
66RGBA_YELLOW_OVERLAY = (0.6, 0.6, 0, 0.6)
67
68LABEL_COLORS = {
69 TestState.ACTIVE: gtk.gdk.color_parse('light goldenrod'),
70 TestState.PASSED: gtk.gdk.color_parse('pale green'),
71 TestState.FAILED: gtk.gdk.color_parse('tomato'),
72 TestState.UNTESTED: gtk.gdk.color_parse('dark slate grey')}
73
74LABEL_FONT = pango.FontDescription('courier new condensed 16')
75
76FAIL_TIMEOUT = 30
77
78USER_PASS_FAIL_SELECT_STR = (
79 'hit TAB to fail and ENTER to pass\n' +
80 '錯誤請按 TAB,成功請按 ENTER')
81
Hung-Te Lin6bb48552012-02-09 14:37:43 +080082_LABEL_EN_SIZE = (170, 35)
83_LABEL_ZH_SIZE = (70, 35)
84_LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
85_LABEL_ZH_FONT = pango.FontDescription('normal 12')
86_LABEL_T_SIZE = (40, 35)
87_LABEL_T_FONT = pango.FontDescription('arial ultra-condensed 10')
88_LABEL_UNTESTED_FG = gtk.gdk.color_parse('grey40')
89_LABEL_TROUGH_COLOR = gtk.gdk.color_parse('grey20')
90_LABEL_STATUS_SIZE = (140, 30)
91_LABEL_STATUS_FONT = pango.FontDescription(
92 'courier new bold extra-condensed 16')
93_OTHER_LABEL_FONT = pango.FontDescription('courier new condensed 20')
94
95_ST_LABEL_EN_SIZE = (250, 35)
96_ST_LABEL_ZH_SIZE = (150, 35)
97
98_NO_ACTIVE_TEST_DELAY_MS = 500
99
100
101# ---------------------------------------------------------------------------
102# Client Library
103
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800104
105def make_label(message, font=LABEL_FONT, fg=LIGHT_GREEN,
106 size=None, alignment=None):
107 l = gtk.Label(message)
108 l.modify_font(font)
109 l.modify_fg(gtk.STATE_NORMAL, fg)
110 if size:
111 l.set_size_request(*size)
112 if alignment:
113 l.set_alignment(*alignment)
114 return l
115
116
117def make_hsep(width=1):
118 frame = gtk.EventBox()
119 frame.set_size_request(-1, width)
120 frame.modify_bg(gtk.STATE_NORMAL, SEP_COLOR)
121 return frame
122
123
124def make_vsep(width=1):
125 frame = gtk.EventBox()
126 frame.set_size_request(width, -1)
127 frame.modify_bg(gtk.STATE_NORMAL, SEP_COLOR)
128 return frame
129
130
131def make_countdown_widget():
132 title = make_label('time remaining / 剩餘時間: ', alignment=(1, 0.5))
133 countdown = make_label('%d' % FAIL_TIMEOUT, alignment=(0, 0.5))
134 hbox = gtk.HBox()
135 hbox.pack_start(title)
136 hbox.pack_start(countdown)
137 eb = gtk.EventBox()
138 eb.modify_bg(gtk.STATE_NORMAL, BLACK)
139 eb.add(hbox)
140 return eb, countdown
141
142
143def hide_cursor(gdk_window):
144 pixmap = gtk.gdk.Pixmap(None, 1, 1, 1)
145 color = gtk.gdk.Color()
146 cursor = gtk.gdk.Cursor(pixmap, pixmap, color, color, 0, 0)
147 gdk_window.set_cursor(cursor)
148
149
150def calc_scale(wanted_x, wanted_y):
151 (widget_size_x, widget_size_y) = factory.get_shared_data('test_widget_size')
152 scale_x = (0.9 * widget_size_x) / wanted_x
153 scale_y = (0.9 * widget_size_y) / wanted_y
154 scale = scale_y if scale_y < scale_x else scale_x
155 scale = 1 if scale > 1 else scale
156 factory.log('scale: %s' % scale)
157 return scale
158
159
160def trim(text, length):
161 if len(text) > length:
162 text = text[:length-3] + '...'
163 return text
164
165
166def make_summary_box(tests, state_map, rows=15):
167 '''
168 Creates a widget display status of a set of test.
169
170 @param tests: A list of FactoryTest nodes whose status (and children's
171 status) should be displayed.
172 @param state_map: The state map as provide by the state instance.
173 @param rows: The number of rows to display.
174 @return: A tuple (widget, label_map), where widget is the widget, and
175 label_map is a map from each test to the corresponding label.
176 '''
177 LABEL_EN_SIZE = (170, 35)
178 LABEL_EN_SIZE_2 = (450, 25)
179 LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
180
181 all_tests = sum([list(t.walk(in_order=True)) for t in tests], [])
182 columns = len(all_tests) / rows + (len(all_tests) % rows != 0)
183
184 info_box = gtk.HBox()
185 info_box.set_spacing(20)
186 for status in (TestState.ACTIVE, TestState.PASSED,
187 TestState.FAILED, TestState.UNTESTED):
188 label = make_label(status,
189 size=LABEL_EN_SIZE,
190 font=LABEL_EN_FONT,
191 alignment=(0.5, 0.5),
192 fg=LABEL_COLORS[status])
193 info_box.pack_start(label, False, False)
194
195 vbox = gtk.VBox()
196 vbox.set_spacing(20)
197 vbox.pack_start(info_box, False, False)
198
199 label_map = {}
200
201 if all_tests:
202 status_table = gtk.Table(rows, columns, True)
203 for (j, i), t in izip(product(xrange(columns), xrange(rows)),
204 all_tests):
205 msg_en = ' ' * (t.depth() - 1) + t.label_en
206 msg_en = trim(msg_en, 12)
207 if t.label_zh:
208 msg = '{0:<12} ({1})'.format(msg_en, t.label_zh)
209 else:
210 msg = msg_en
211 status = state_map[t].status
212 status_label = make_label(msg,
213 size=LABEL_EN_SIZE_2,
214 font=LABEL_EN_FONT,
215 alignment=(0.0, 0.5),
216 fg=LABEL_COLORS[status])
217 label_map[t] = status_label
218 status_table.attach(status_label, j, j+1, i, i+1)
219 vbox.pack_start(status_table, False, False)
220
221 return vbox, label_map
222
223
224def run_test_widget(dummy_job, test_widget,
225 invisible_cursor=True,
226 window_registration_callback=None,
227 cleanup_callback=None):
228 test_widget_size = factory.get_shared_data('test_widget_size')
229
230 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
231 window.modify_bg(gtk.STATE_NORMAL, BLACK)
232 window.set_size_request(*test_widget_size)
233
234 def show_window():
235 window.show()
236 window.window.raise_() # pylint: disable=E1101
237 gtk.gdk.pointer_grab(window.window, confine_to=window.window)
238 if invisible_cursor:
239 hide_cursor(window.window)
240
241 test_path = factory.get_current_test_path()
242
243 def handle_event(event):
244 if (event.type == Event.Type.STATE_CHANGE and
245 test_path and event.path == test_path and
246 event.state.visible):
247 show_window()
248
249 event_client = EventClient(
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800250 callback=handle_event, event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800251
252 align = gtk.Alignment(xalign=0.5, yalign=0.5)
253 align.add(test_widget)
254
255 window.add(align)
256 for c in window.get_children():
257 # Show all children, but not the window itself yet.
258 c.show_all()
259
260 if window_registration_callback is not None:
261 window_registration_callback(window)
262
263 # Show the window if it is the visible test, or if the test_path is not
264 # available (e.g., run directly from the command line).
265 if (not test_path) or (
266 TestState.from_dict_or_object(
267 factory.get_state_instance().get_test_state(test_path)).visible):
268 show_window()
269 else:
270 window.hide()
271
272 gtk.main()
273
274 gtk.gdk.pointer_ungrab()
275
276 if cleanup_callback is not None:
277 cleanup_callback()
278
279 del event_client
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800280
281
282# ---------------------------------------------------------------------------
283# Server Implementation
284
285
286class Console(object):
287 '''Display a progress log. Implemented by launching an borderless
288 xterm at a strategic location, and running tail against the log.'''
289
290 def __init__(self, allocation):
291 xterm_coords = '145x13+%d+%d' % (allocation.x, allocation.y)
292 logging.info('xterm_coords = %s', xterm_coords)
293 xterm_opts = ('-bg black -fg lightgray -bw 0 -g %s' % xterm_coords)
294 xterm_cmd = (('urxvt %s -e bash -c ' % xterm_opts).split() +
295 ['tail -f "%s"' % factory.CONSOLE_LOG_PATH])
296 logging.info('xterm_cmd = %s', xterm_cmd)
297 self._proc = subprocess.Popen(xterm_cmd)
298
299 def __del__(self):
300 logging.info('console_proc __del__')
301 self._proc.kill()
302
303
304class TestLabelBox(gtk.EventBox): # pylint: disable=R0904
305
306 def __init__(self, test, show_shortcut=False):
307 gtk.EventBox.__init__(self)
308 self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[TestState.UNTESTED])
309
310 label_en = make_label(test.label_en, size=_LABEL_EN_SIZE,
311 font=_LABEL_EN_FONT, alignment=(0.5, 0.5),
312 fg=_LABEL_UNTESTED_FG)
313 label_zh = make_label(test.label_zh, size=_LABEL_ZH_SIZE,
314 font=_LABEL_ZH_FONT, alignment=(0.5, 0.5),
315 fg=_LABEL_UNTESTED_FG)
316 label_t = make_label('C-' + test.kbd_shortcut.upper(),
317 size=_LABEL_T_SIZE, font=_LABEL_T_FONT,
318 alignment=(0.5, 0.5), fg=BLACK)
319
320 # build a better label_en with shortcuts
321 index_hotkey = test.label_en.upper().find(test.kbd_shortcut.upper())
322 if show_shortcut and index_hotkey >= 0:
323 attrs = label_en.get_attributes() or pango.AttrList()
324 attrs.insert(pango.AttrUnderline(
325 pango.UNDERLINE_LOW, index_hotkey, index_hotkey + 1))
326 attrs.insert(pango.AttrWeight(
327 pango.WEIGHT_BOLD, index_hotkey, index_hotkey + 1))
328 label_en.set_attributes(attrs)
329
330 hbox = gtk.HBox()
331 hbox.pack_start(label_en, False, False)
332 hbox.pack_start(label_zh, False, False)
333 hbox.pack_start(label_t, False, False)
334 self.add(hbox)
335 self.label_list = [label_en, label_zh]
336
337 def update(self, state):
338 label_fg = (_LABEL_UNTESTED_FG if state.status == TestState.UNTESTED
339 else BLACK)
340 for label in self.label_list:
341 label.modify_fg(gtk.STATE_NORMAL, label_fg)
342 self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[state.status])
343 self.queue_draw()
344
345
346class UiState(object):
347
348 def __init__(self, test_widget_box):
349 self._test_widget_box = test_widget_box
350 self._label_box_map = {}
351 self._transition_count = 0
352
353 self._active_test_label_map = None
354
355 def _remove_state_widget(self):
356 """Remove any existing state widgets."""
357 for child in self._test_widget_box.get_children():
358 self._test_widget_box.remove(child)
359 self._active_test_label_map = None
360
361 def update_test_label(self, test, state):
362 label_box = self._label_box_map.get(test)
363 if label_box:
364 label_box.update(state)
365
366 def update_test_state(self, test_list, state_map):
367 active_tests = [
368 t for t in test_list.walk()
369 if t.is_leaf() and state_map[t].status == TestState.ACTIVE]
370 has_active_ui = any(t.has_ui for t in active_tests)
371
372 if not active_tests:
373 # Display the "no active tests" widget if there are still no
374 # active tests after _NO_ACTIVE_TEST_DELAY_MS.
375 def run(transition_count):
376 if transition_count != self._transition_count:
377 # Something has happened
378 return False
379
380 self._transition_count += 1
381 self._remove_state_widget()
382
383 self._test_widget_box.set(0.5, 0.5, 0.0, 0.0)
384 self._test_widget_box.set_padding(0, 0, 0, 0)
385 label_box = gtk.EventBox()
386 label_box.modify_bg(gtk.STATE_NORMAL, BLACK)
387 label = make_label('no active test', font=_OTHER_LABEL_FONT,
388 alignment=(0.5, 0.5))
389 label_box.add(label)
390 self._test_widget_box.add(label_box)
391 self._test_widget_box.show_all()
392
393 gobject.timeout_add(_NO_ACTIVE_TEST_DELAY_MS, run,
394 self._transition_count)
395 return
396
397 self._transition_count += 1
398
399 if has_active_ui:
400 # Remove the widget (if any) since there is an active test
401 # with a UI.
402 self._remove_state_widget()
403 return
404
405 if (self._active_test_label_map is not None and
406 all(t in self._active_test_label_map for t in active_tests)):
407 # All active tests are already present in the summary, so just
408 # update their states.
409 for test, label in self._active_test_label_map.iteritems():
410 label.modify_fg(
411 gtk.STATE_NORMAL,
412 LABEL_COLORS[state_map[test].status])
413 return
414
415 self._remove_state_widget()
416 # No active UI; draw summary of current test states
417 self._test_widget_box.set(0.5, 0.0, 0.0, 0.0)
418 self._test_widget_box.set_padding(40, 0, 0, 0)
419 vbox, self._active_test_label_map = make_summary_box(
420 [t for t in test_list.subtests
421 if state_map[t].status == TestState.ACTIVE],
422 state_map)
423 self._test_widget_box.add(vbox)
424 self._test_widget_box.show_all()
425
426 def set_label_box(self, test, label_box):
427 self._label_box_map[test] = label_box
428
429
430def main(test_list_path):
431 '''Starts the main UI.
432
433 This is launched by the autotest/cros/factory/client.
434 When operators press keyboard shortcuts, the shortcut
435 value is sent as an event to the control program.'''
436
437 test_list = None
438 ui_state = None
439 event_client = None
440
441 # Delay loading Xlib because Xlib is currently not available in image build
442 # process host-depends list, and it's only required by the main UI, not all
443 # the tests using UI library (in other words, it'll be slower and break the
444 # build system if Xlib is globally imported).
445 try:
446 from Xlib import X
447 from Xlib.display import Display
448 disp = Display()
449 except:
450 logging.error('Failed loading X modules')
451 raise
452
453 def handle_key_release_event(_, event):
454 logging.info('base ui key event (%s)', event.keyval)
455 return True
456
457 def handle_event(event):
458 if event.type == Event.Type.STATE_CHANGE:
459 test = test_list.lookup_path(event.path)
460 state_map = test_list.get_state_map()
461 ui_state.update_test_label(test, state_map[test])
462 ui_state.update_test_state(test_list, state_map)
463
464 def grab_shortcut_keys(kbd_shortcuts):
465 root = disp.screen().root
466 keycode_map = {}
467
468 def handle_xevent( # pylint: disable=W0102
469 dummy_src, dummy_cond, xhandle=root.display,
470 keycode_map=keycode_map):
471 for dummy_i in range(0, xhandle.pending_events()):
472 xevent = xhandle.next_event()
473 if xevent.type == X.KeyPress:
474 keycode = xevent.detail
475 event_client.post_event(Event('kbd_shortcut',
476 key=keycode_map[keycode]))
477 return True
478
479 # We want to receive KeyPress events
480 root.change_attributes(event_mask = X.KeyPressMask)
481
482 for mod, shortcut in ([(X.ControlMask, k) for k in kbd_shortcuts] +
483 [(X.Mod1Mask, 'Tab')]): # Mod1 = Alt
484 keysym = gtk.gdk.keyval_from_name(shortcut)
485 keycode = disp.keysym_to_keycode(keysym)
486 keycode_map[keycode] = shortcut
487 root.grab_key(keycode, mod, 1,
488 X.GrabModeAsync, X.GrabModeAsync)
489
490 # This flushes the XGrabKey calls to the server.
491 for dummy_x in range(0, root.display.pending_events()):
492 root.display.next_event()
493 gobject.io_add_watch(root.display, gobject.IO_IN, handle_xevent)
494
495
496 test_list = factory.read_test_list(test_list_path)
497
498 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
499 window.connect('destroy', lambda _: gtk.main_quit())
500 window.modify_bg(gtk.STATE_NORMAL, BLACK)
501
502 event_client = EventClient(
503 callback=handle_event,
504 event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
505
506 screen = window.get_screen()
507 if (screen is None):
508 logging.info('ERROR: communication with the X server is not working, ' +
509 'could not find a working screen. UI exiting.')
510 sys.exit(1)
511
512 screen_size_str = os.environ.get('CROS_SCREEN_SIZE')
513 if screen_size_str:
514 match = re.match(r'^(\d+)x(\d+)$', screen_size_str)
515 assert match, 'CROS_SCREEN_SIZE should be {width}x{height}'
516 screen_size = (int(match.group(1)), int(match.group(2)))
517 else:
518 screen_size = (screen.get_width(), screen.get_height())
519 window.set_size_request(*screen_size)
520
521 label_trough = gtk.VBox()
522 label_trough.set_spacing(0)
523
524 rhs_box = gtk.EventBox()
525 rhs_box.modify_bg(gtk.STATE_NORMAL, _LABEL_TROUGH_COLOR)
526 rhs_box.add(label_trough)
527
528 console_box = gtk.EventBox()
529 console_box.set_size_request(-1, 180)
530 console_box.modify_bg(gtk.STATE_NORMAL, BLACK)
531
532 test_widget_box = gtk.Alignment()
533 test_widget_box.set_size_request(-1, -1)
534
535 lhs_box = gtk.VBox()
536 lhs_box.pack_end(console_box, False, False)
537 lhs_box.pack_start(test_widget_box)
538 lhs_box.pack_start(make_hsep(3), False, False)
539
540 base_box = gtk.HBox()
541 base_box.pack_end(rhs_box, False, False)
542 base_box.pack_end(make_vsep(3), False, False)
543 base_box.pack_start(lhs_box)
544
545 window.connect('key-release-event', handle_key_release_event)
546 window.add_events(gtk.gdk.KEY_RELEASE_MASK)
547
548 ui_state = UiState(test_widget_box)
549
550 for test in test_list.subtests:
551 label_box = TestLabelBox(test, True)
552 ui_state.set_label_box(test, label_box)
553 label_trough.pack_start(label_box, False, False)
554 label_trough.pack_start(make_hsep(), False, False)
555
556 window.add(base_box)
557 window.show_all()
558
559 state_map = test_list.get_state_map()
560 for test, state in test_list.get_state_map().iteritems():
561 ui_state.update_test_label(test, state)
562 ui_state.update_test_state(test_list, state_map)
563
564 grab_shortcut_keys(test_list.kbd_shortcut_map.keys())
565
566 hide_cursor(window.window)
567
568 test_widget_allocation = test_widget_box.get_allocation()
569 test_widget_size = (test_widget_allocation.width,
570 test_widget_allocation.height)
571 factory.set_shared_data('test_widget_size', test_widget_size)
572
573 dummy_console = Console(console_box.get_allocation())
574
575 event_client.post_event(Event(Event.Type.UI_READY))
576
577 logging.info('cros/factory/ui setup done, starting gtk.main()...')
578 gtk.main()
579 logging.info('cros/factory/ui gtk.main() finished, exiting.')
580
581
582if __name__ == '__main__':
583 if len(sys.argv) != 2:
584 print 'usage: %s <test list path>' % sys.argv[0]
585 sys.exit(1)
586
587 factory.init_logging("cros/factory/ui", verbose=True)
588 main(sys.argv[1])