blob: 581c4673a2e8846d60f03d5d6b688dbe7ef83fdc [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
Tammo Spalinkdb8d7112012-03-13 18:54:37 +080034import string
Hung-Te Lin6bb48552012-02-09 14:37:43 +080035import subprocess
36import sys
Hung-Te Linf2f78f72012-02-08 19:27:11 +080037from itertools import izip, product
Jon Salz14bcbb02012-03-17 15:11:50 +080038from optparse import OptionParser
Hung-Te Linf2f78f72012-02-08 19:27:11 +080039
Hung-Te Lin6bb48552012-02-09 14:37:43 +080040# GTK and X modules
41import gobject
42import gtk
43import pango
44
Tammo Spalinkdb8d7112012-03-13 18:54:37 +080045# Guard loading Xlib because it is currently not available in the
46# image build process host-depends list. Failure to load in
47# production should always manifest during regular use.
48try:
49 from Xlib import X
50 from Xlib.display import Display
51except:
52 pass
53
Hung-Te Lin6bb48552012-02-09 14:37:43 +080054# Factory and autotest modules
Hung-Te Linf2f78f72012-02-08 19:27:11 +080055import factory_common
Hung-Te Linde45e9c2012-03-19 13:02:06 +080056from autotest_lib.client.common_lib import error
Hung-Te Linf2f78f72012-02-08 19:27:11 +080057from autotest_lib.client.cros import factory
58from autotest_lib.client.cros.factory import TestState
59from autotest_lib.client.cros.factory.event import Event, EventClient
60
Hung-Te Lin6bb48552012-02-09 14:37:43 +080061
Hung-Te Linf2f78f72012-02-08 19:27:11 +080062# For compatibility with tests before TestState existed
63ACTIVE = TestState.ACTIVE
64PASSED = TestState.PASSED
65FAILED = TestState.FAILED
66UNTESTED = TestState.UNTESTED
67
Hung-Te Line94e0a02012-03-19 18:20:35 +080068# Arrow symbols
69SYMBOL_RIGHT_ARROW = u'\u25b8'
70SYMBOL_DOWN_ARROW = u'\u25bc'
71
Hung-Te Lin6bb48552012-02-09 14:37:43 +080072# Color definition
Hung-Te Linf2f78f72012-02-08 19:27:11 +080073BLACK = gtk.gdk.Color()
74RED = gtk.gdk.Color(0xFFFF, 0, 0)
75GREEN = gtk.gdk.Color(0, 0xFFFF, 0)
76BLUE = gtk.gdk.Color(0, 0, 0xFFFF)
77WHITE = gtk.gdk.Color(0xFFFF, 0xFFFF, 0xFFFF)
Hung-Te Linf2f78f72012-02-08 19:27:11 +080078LIGHT_GREEN = gtk.gdk.color_parse('light green')
Hung-Te Linf2f78f72012-02-08 19:27:11 +080079SEP_COLOR = gtk.gdk.color_parse('grey50')
80
81RGBA_GREEN_OVERLAY = (0, 0.5, 0, 0.6)
82RGBA_YELLOW_OVERLAY = (0.6, 0.6, 0, 0.6)
83
84LABEL_COLORS = {
85 TestState.ACTIVE: gtk.gdk.color_parse('light goldenrod'),
86 TestState.PASSED: gtk.gdk.color_parse('pale green'),
87 TestState.FAILED: gtk.gdk.color_parse('tomato'),
88 TestState.UNTESTED: gtk.gdk.color_parse('dark slate grey')}
89
90LABEL_FONT = pango.FontDescription('courier new condensed 16')
Hung-Te Linbf545582012-02-15 17:08:07 +080091LABEL_LARGE_FONT = pango.FontDescription('courier new condensed 24')
Hung-Te Linf2f78f72012-02-08 19:27:11 +080092
93FAIL_TIMEOUT = 30
94
Hung-Te Line94e0a02012-03-19 18:20:35 +080095MESSAGE_NO_ACTIVE_TESTS = (
96 "No more tests to run. To re-run items, press shortcuts\n"
97 "from the test list in right side or from following list:\n\n"
98 "Ctrl-Alt-A (Auto-Run):\n"
99 " Test remaining untested items.\n\n"
100 "Ctrl-Alt-F (Re-run Failed):\n"
101 " Re-test failed and untested items.\n\n"
102 "Ctrl-Alt-R (Reset):\n"
103 " Re-test everything.\n\n"
104 )
105
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800106USER_PASS_FAIL_SELECT_STR = (
107 'hit TAB to fail and ENTER to pass\n' +
108 '錯誤請按 TAB,成功請按 ENTER')
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800109# Resolution where original UI is designed for.
110_UI_SCREEN_WIDTH = 1280
111_UI_SCREEN_HEIGHT = 800
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800112
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800113_LABEL_STATUS_ROW_SIZE = (300, 30)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800114_LABEL_EN_SIZE = (170, 35)
115_LABEL_ZH_SIZE = (70, 35)
116_LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
117_LABEL_ZH_FONT = pango.FontDescription('normal 12')
118_LABEL_T_SIZE = (40, 35)
119_LABEL_T_FONT = pango.FontDescription('arial ultra-condensed 10')
120_LABEL_UNTESTED_FG = gtk.gdk.color_parse('grey40')
121_LABEL_TROUGH_COLOR = gtk.gdk.color_parse('grey20')
122_LABEL_STATUS_SIZE = (140, 30)
123_LABEL_STATUS_FONT = pango.FontDescription(
124 'courier new bold extra-condensed 16')
125_OTHER_LABEL_FONT = pango.FontDescription('courier new condensed 20')
126
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800127_NO_ACTIVE_TEST_DELAY_MS = 500
128
Jon Salz0405ab52012-03-16 15:26:52 +0800129GLOBAL_HOT_KEY_EVENTS = {
130 'r': Event.Type.RESTART_TESTS,
131 'a': Event.Type.AUTO_RUN,
132 'f': Event.Type.RE_RUN_FAILED,
133 }
134try:
135 # Works only if X is available.
136 GLOBAL_HOT_KEY_MASK = X.ControlMask | X.Mod1Mask
137except:
138 pass
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800139
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800140# ---------------------------------------------------------------------------
141# Client Library
142
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800143
Hung-Te Lin4b0b61f2012-03-05 14:20:06 +0800144# TODO(hungte) Replace gtk_lock by gtk.gdk.lock when it's availble (need pygtk
145# 2.2x, and we're now pinned by 2.1x)
146class _GtkLock(object):
147 __enter__ = gtk.gdk.threads_enter
148 def __exit__(*ignored):
149 gtk.gdk.threads_leave()
150
151
152gtk_lock = _GtkLock()
153
154
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800155def make_label(message, font=LABEL_FONT, fg=LIGHT_GREEN,
156 size=None, alignment=None):
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800157 """Returns a label widget.
158
159 A wrapper for gtk.Label. The unit of size is pixels under resolution
160 _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
161
162 @param message: A string to be displayed.
163 @param font: Font descriptor for the label.
164 @param fg: Foreground color.
165 @param size: Minimum size for this label.
166 @param alignment: Alignment setting.
167 @return: A label widget.
168 """
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800169 l = gtk.Label(message)
170 l.modify_font(font)
171 l.modify_fg(gtk.STATE_NORMAL, fg)
172 if size:
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800173 # Convert size according to the current resolution.
174 l.set_size_request(*convert_pixels(size))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800175 if alignment:
176 l.set_alignment(*alignment)
177 return l
178
179
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800180def make_status_row(init_prompt,
181 init_status,
182 label_size=_LABEL_STATUS_ROW_SIZE):
183 """Returns a widget that live updates prompt and status in a row.
184
185 Args:
186 init_prompt: The prompt label text.
187 init_status: The status label text.
188 label_size: The desired size of the prompt label and the status label.
189
190 Returns:
191 1) A dict whose content is linked by the widget.
192 2) A widget to render dict content in "prompt: status" format.
193 """
194 display_dict = {}
195 display_dict['prompt'] = init_prompt
196 display_dict['status'] = init_status
197
198 def prompt_label_expose(widget, event):
199 prompt = display_dict['prompt']
200 widget.set_text(prompt)
201
202 def status_label_expose(widget, event):
203 status = display_dict['status']
204 widget.set_text(status)
205 widget.modify_fg(gtk.STATE_NORMAL, LABEL_COLORS[status])
206
207 prompt_label = make_label(
208 init_prompt, size=label_size,
209 alignment=(0, 0.5))
210 delimiter_label = make_label(':', alignment=(0, 0.5))
211 status_label = make_label(
212 init_status, size=label_size,
213 alignment=(0, 0.5), fg=LABEL_COLORS[init_status])
214
215 widget = gtk.HBox()
216 widget.pack_end(status_label, False, False)
217 widget.pack_end(delimiter_label, False, False)
218 widget.pack_end(prompt_label, False, False)
219
220 status_label.connect('expose_event', status_label_expose)
221 prompt_label.connect('expose_event', prompt_label_expose)
222 return display_dict, widget
223
224
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800225def convert_pixels(size):
226 """Converts a pair in pixel that is suitable for current resolution.
227
228 GTK takes pixels as its unit in many function calls. To maintain the
229 consistency of the UI in different resolution, a conversion is required.
230 Take current resolution and (_UI_SCREEN_WIDTH, _UI_SCREEN_HEIGHT) as
231 the original resolution, this function returns a pair of width and height
232 that is converted for current resolution.
233
234 Because pixels in negative usually indicates unspecified, no conversion
235 will be done for negative pixels.
236
237 In addition, the aspect ratio is not maintained in this function.
238
239 Usage Example:
240 width,_ = convert_pixels((20,-1))
241
242 @param size: A pair of pixels that designed under original resolution.
243 @return: A pair of pixels of (width, height) format.
244 Pixels returned are always integer.
245 """
246 return (int(float(size[0]) / _UI_SCREEN_WIDTH * gtk.gdk.screen_width()
247 if (size[0] > 0) else size[0]),
248 int(float(size[1]) / _UI_SCREEN_HEIGHT * gtk.gdk.screen_height()
249 if (size[1] > 0) else size[1]))
250
251
252def make_hsep(height=1):
253 """Returns a widget acts as a horizontal separation line.
254
255 The unit is pixels under resolution _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
256 """
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800257 frame = gtk.EventBox()
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800258 # Convert height according to the current resolution.
259 frame.set_size_request(*convert_pixels((-1, height)))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800260 frame.modify_bg(gtk.STATE_NORMAL, SEP_COLOR)
261 return frame
262
263
264def make_vsep(width=1):
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800265 """Returns a widget acts as a vertical separation line.
266
267 The unit is pixels under resolution _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
268 """
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800269 frame = gtk.EventBox()
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800270 # Convert width according to the current resolution.
271 frame.set_size_request(*convert_pixels((width, -1)))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800272 frame.modify_bg(gtk.STATE_NORMAL, SEP_COLOR)
273 return frame
274
275
Hung-Te Linaf8ec542012-03-14 20:01:40 +0800276def make_countdown_widget(prompt=None, value=None, fg=LIGHT_GREEN):
277 if prompt is None:
278 prompt = 'time remaining / 剩餘時間: '
279 if value is None:
280 value = '%s' % FAIL_TIMEOUT
281 title = make_label(prompt, fg=fg, alignment=(1, 0.5))
282 countdown = make_label(value, fg=fg, alignment=(0, 0.5))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800283 hbox = gtk.HBox()
284 hbox.pack_start(title)
285 hbox.pack_start(countdown)
286 eb = gtk.EventBox()
287 eb.modify_bg(gtk.STATE_NORMAL, BLACK)
288 eb.add(hbox)
289 return eb, countdown
290
291
292def hide_cursor(gdk_window):
293 pixmap = gtk.gdk.Pixmap(None, 1, 1, 1)
294 color = gtk.gdk.Color()
295 cursor = gtk.gdk.Cursor(pixmap, pixmap, color, color, 0, 0)
296 gdk_window.set_cursor(cursor)
297
298
299def calc_scale(wanted_x, wanted_y):
300 (widget_size_x, widget_size_y) = factory.get_shared_data('test_widget_size')
301 scale_x = (0.9 * widget_size_x) / wanted_x
302 scale_y = (0.9 * widget_size_y) / wanted_y
303 scale = scale_y if scale_y < scale_x else scale_x
304 scale = 1 if scale > 1 else scale
305 factory.log('scale: %s' % scale)
306 return scale
307
308
309def trim(text, length):
310 if len(text) > length:
311 text = text[:length-3] + '...'
312 return text
313
314
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800315class InputError(ValueError):
316 """Execption for input window callbacks to change status text message."""
317 pass
318
319
Hung-Te Linbf545582012-02-15 17:08:07 +0800320def make_input_window(prompt=None,
321 init_value=None,
322 msg_invalid=None,
323 font=None,
324 on_validate=None,
325 on_keypress=None,
326 on_complete=None):
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800327 """Creates a widget to prompt user for a valid string.
Hung-Te Linbf545582012-02-15 17:08:07 +0800328
329 @param prompt: A string to be displayed. None for default message.
330 @param init_value: Initial value to be set.
331 @param msg_invalid: Status string to display when input is invalid. None for
332 default message.
333 @param font: Font specification (string or pango.FontDescription) for label
334 and entry. None for default large font.
335 @param on_validate: A callback function to validate if the input from user
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800336 is valid. None for allowing any non-empty input. Any ValueError or
337 ui.InputError raised during execution in on_validate will be displayed
338 in bottom status.
Hung-Te Linbf545582012-02-15 17:08:07 +0800339 @param on_keypress: A callback function when each keystroke is hit.
340 @param on_complete: A callback function when a valid string is passed.
341 None to stop (gtk.main_quit).
342 @return: A widget with prompt, input entry, and status label.
Chun-ta Lin2e7f44c2012-03-03 07:29:36 +0800343 In addition, a method called get_entry() is added to the widget to
344 provide controls on the entry.
Hung-Te Linbf545582012-02-15 17:08:07 +0800345 """
346 DEFAULT_MSG_INVALID = "Invalid input / 輸入不正確"
347 DEFAULT_PROMPT = "Enter Data / 輸入資料:"
348
349 def enter_callback(entry):
350 text = entry.get_text()
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800351 try:
352 if (on_validate and (not on_validate(text))) or (not text.strip()):
353 raise ValueError(msg_invalid)
Hung-Te Linbf545582012-02-15 17:08:07 +0800354 on_complete(text) if on_complete else gtk.main_quit()
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800355 except ValueError as e:
356 gtk.gdk.beep()
357 status_label.set_text('ERROR: %s' % e.message)
Hung-Te Linbf545582012-02-15 17:08:07 +0800358 return True
359
360 def key_press_callback(entry, key):
361 status_label.set_text('')
362 if on_keypress:
363 return on_keypress(entry, key)
364 return False
365
366 # Populate default parameters
367 if msg_invalid is None:
368 msg_invalid = DEFAULT_MSG_INVALID
369
370 if prompt is None:
371 prompt = DEFAULT_PROMPT
372
373 if font is None:
374 font = LABEL_LARGE_FONT
375 elif not isinstance(font, pango.FontDescription):
376 font = pango.FontDescription(font)
377
378 widget = gtk.VBox()
379 label = make_label(prompt, font=font)
380 status_label = make_label('', font=font)
381 entry = gtk.Entry()
382 entry.modify_font(font)
383 entry.connect("activate", enter_callback)
384 entry.connect("key_press_event", key_press_callback)
385 if init_value:
386 entry.set_text(init_value)
387 widget.modify_bg(gtk.STATE_NORMAL, BLACK)
388 status_label.modify_fg(gtk.STATE_NORMAL, RED)
389 widget.add(label)
390 widget.pack_start(entry)
391 widget.pack_start(status_label)
Chun-ta Lin2e7f44c2012-03-03 07:29:36 +0800392
393 # Method for getting the entry.
394 widget.get_entry = lambda : entry
Hung-Te Linbf545582012-02-15 17:08:07 +0800395 return widget
396
397
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800398def make_summary_box(tests, state_map, rows=15):
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800399 '''Creates a widget display status of a set of test.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800400
401 @param tests: A list of FactoryTest nodes whose status (and children's
402 status) should be displayed.
403 @param state_map: The state map as provide by the state instance.
404 @param rows: The number of rows to display.
405 @return: A tuple (widget, label_map), where widget is the widget, and
406 label_map is a map from each test to the corresponding label.
407 '''
408 LABEL_EN_SIZE = (170, 35)
409 LABEL_EN_SIZE_2 = (450, 25)
410 LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
411
412 all_tests = sum([list(t.walk(in_order=True)) for t in tests], [])
413 columns = len(all_tests) / rows + (len(all_tests) % rows != 0)
414
415 info_box = gtk.HBox()
416 info_box.set_spacing(20)
417 for status in (TestState.ACTIVE, TestState.PASSED,
418 TestState.FAILED, TestState.UNTESTED):
419 label = make_label(status,
420 size=LABEL_EN_SIZE,
421 font=LABEL_EN_FONT,
422 alignment=(0.5, 0.5),
423 fg=LABEL_COLORS[status])
424 info_box.pack_start(label, False, False)
425
426 vbox = gtk.VBox()
427 vbox.set_spacing(20)
428 vbox.pack_start(info_box, False, False)
429
430 label_map = {}
431
432 if all_tests:
433 status_table = gtk.Table(rows, columns, True)
434 for (j, i), t in izip(product(xrange(columns), xrange(rows)),
435 all_tests):
436 msg_en = ' ' * (t.depth() - 1) + t.label_en
437 msg_en = trim(msg_en, 12)
438 if t.label_zh:
439 msg = '{0:<12} ({1})'.format(msg_en, t.label_zh)
440 else:
441 msg = msg_en
442 status = state_map[t].status
443 status_label = make_label(msg,
444 size=LABEL_EN_SIZE_2,
445 font=LABEL_EN_FONT,
446 alignment=(0.0, 0.5),
447 fg=LABEL_COLORS[status])
448 label_map[t] = status_label
449 status_table.attach(status_label, j, j+1, i, i+1)
450 vbox.pack_start(status_table, False, False)
451
452 return vbox, label_map
453
454
455def run_test_widget(dummy_job, test_widget,
456 invisible_cursor=True,
457 window_registration_callback=None,
458 cleanup_callback=None):
459 test_widget_size = factory.get_shared_data('test_widget_size')
460
461 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
462 window.modify_bg(gtk.STATE_NORMAL, BLACK)
463 window.set_size_request(*test_widget_size)
464
465 def show_window():
466 window.show()
467 window.window.raise_() # pylint: disable=E1101
468 gtk.gdk.pointer_grab(window.window, confine_to=window.window)
469 if invisible_cursor:
470 hide_cursor(window.window)
471
472 test_path = factory.get_current_test_path()
473
474 def handle_event(event):
475 if (event.type == Event.Type.STATE_CHANGE and
476 test_path and event.path == test_path and
477 event.state.visible):
478 show_window()
479
480 event_client = EventClient(
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800481 callback=handle_event, event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800482
483 align = gtk.Alignment(xalign=0.5, yalign=0.5)
484 align.add(test_widget)
485
486 window.add(align)
487 for c in window.get_children():
488 # Show all children, but not the window itself yet.
489 c.show_all()
490
491 if window_registration_callback is not None:
492 window_registration_callback(window)
493
494 # Show the window if it is the visible test, or if the test_path is not
495 # available (e.g., run directly from the command line).
496 if (not test_path) or (
497 TestState.from_dict_or_object(
498 factory.get_state_instance().get_test_state(test_path)).visible):
499 show_window()
500 else:
501 window.hide()
502
Hung-Te Linde45e9c2012-03-19 13:02:06 +0800503 # When gtk.main() is running, it ignores all uncaught exceptions, which is
504 # not preferred by most of our factory tests. To prevent writing special
505 # function raising errors, we hook top level exception handler to always
506 # leave GTK main and raise exception again.
507
508 def exception_hook(exc_type, value, traceback):
509 # Prevent re-entrant.
510 sys.excepthook = old_excepthook
511 session['exception'] = (exc_type, value, traceback)
512 gobject.idle_add(gtk.main_quit)
513 return old_excepthook(exc_type, value, traceback)
514
515 session = {}
516 old_excepthook = sys.excepthook
517 sys.excepthook = exception_hook
518
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800519 gtk.main()
520
521 gtk.gdk.pointer_ungrab()
522
523 if cleanup_callback is not None:
524 cleanup_callback()
525
526 del event_client
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800527
Hung-Te Linde45e9c2012-03-19 13:02:06 +0800528 sys.excepthook = old_excepthook
529 exc_info = session.get('exception')
530 if exc_info is not None:
531 logging.error(exc_info[0], exc_info=exc_info)
532 raise error.TestError(exc_info[1])
533
534
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800535
536# ---------------------------------------------------------------------------
537# Server Implementation
538
539
540class Console(object):
541 '''Display a progress log. Implemented by launching an borderless
542 xterm at a strategic location, and running tail against the log.'''
543
544 def __init__(self, allocation):
Chun-Ta Lin734e96a2012-03-16 20:05:33 +0800545 # Specify how many lines and characters per line are displayed.
546 XTERM_DISPLAY_LINES = 13
547 XTERM_DISPLAY_CHARS = 120
548 # Extra space reserved for pixels between lines.
549 XTERM_RESERVED_LINES = 3
550
551 xterm_coords = '%dx%d+%d+%d' % (XTERM_DISPLAY_CHARS,
552 XTERM_DISPLAY_LINES,
553 allocation.x,
554 allocation.y)
555 xterm_reserved_height = gtk.gdk.screen_height() - allocation.y
556 font_size = int(float(xterm_reserved_height) / (XTERM_DISPLAY_LINES +
557 XTERM_RESERVED_LINES))
558 logging.info('xterm_reserved_height = %d' % xterm_reserved_height)
559 logging.info('font_size = %d' % font_size)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800560 logging.info('xterm_coords = %s', xterm_coords)
561 xterm_opts = ('-bg black -fg lightgray -bw 0 -g %s' % xterm_coords)
Chun-Ta Lin734e96a2012-03-16 20:05:33 +0800562 xterm_cmd = (
563 ['urxvt'] + xterm_opts.split() +
564 ['-fn', 'xft:DejaVu Sans Mono:pixelsize=%s' % font_size] +
565 ['-e', 'bash'] +
566 ['-c', 'tail -f "%s"' % factory.CONSOLE_LOG_PATH])
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800567 logging.info('xterm_cmd = %s', xterm_cmd)
568 self._proc = subprocess.Popen(xterm_cmd)
569
570 def __del__(self):
571 logging.info('console_proc __del__')
572 self._proc.kill()
573
574
575class TestLabelBox(gtk.EventBox): # pylint: disable=R0904
576
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800577 def __init__(self, test):
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800578 gtk.EventBox.__init__(self)
579 self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[TestState.UNTESTED])
Hung-Te Line94e0a02012-03-19 18:20:35 +0800580 self._is_group = test.is_group()
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800581 depth = len(test.get_ancestor_groups())
Hung-Te Line94e0a02012-03-19 18:20:35 +0800582 self._label_text = ' %s%s%s' % (
583 ' ' * depth,
584 SYMBOL_RIGHT_ARROW if self._is_group else ' ',
585 test.label_en)
586 if self._is_group:
587 self._label_text_collapsed = ' %s%s%s' % (
588 ' ' * depth,
589 SYMBOL_DOWN_ARROW if self._is_group else '',
590 test.label_en)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800591 self._label_en = make_label(
Hung-Te Line94e0a02012-03-19 18:20:35 +0800592 self._label_text, size=_LABEL_EN_SIZE,
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800593 font=_LABEL_EN_FONT, alignment=(0, 0.5),
594 fg=_LABEL_UNTESTED_FG)
595 self._label_zh = make_label(
596 test.label_zh, size=_LABEL_ZH_SIZE,
597 font=_LABEL_ZH_FONT, alignment=(0.5, 0.5),
598 fg=_LABEL_UNTESTED_FG)
599 self._label_t = make_label(
600 '', size=_LABEL_T_SIZE, font=_LABEL_T_FONT,
601 alignment=(0.5, 0.5), fg=BLACK)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800602 hbox = gtk.HBox()
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800603 hbox.pack_start(self._label_en, False, False)
604 hbox.pack_start(self._label_zh, False, False)
605 hbox.pack_start(self._label_t, False, False)
606 vbox = gtk.VBox()
607 vbox.pack_start(hbox, False, False)
608 vbox.pack_start(make_hsep(), False, False)
609 self.add(vbox)
610 self._status = None
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800611
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800612 def set_shortcut(self, shortcut):
613 if shortcut is None:
614 return
615 self._label_t.set_text('C-%s' % shortcut)
616 attrs = self._label_en.get_attributes() or pango.AttrList()
617 attrs.filter(lambda attr: attr.type == pango.ATTR_UNDERLINE)
618 index_hotkey = self._label_en.get_text().upper().find(shortcut.upper())
619 if index_hotkey != -1:
620 attrs.insert(pango.AttrUnderline(
621 pango.UNDERLINE_LOW, index_hotkey, index_hotkey + 1))
622 attrs.insert(pango.AttrWeight(
623 pango.WEIGHT_BOLD, index_hotkey, index_hotkey + 1))
624 self._label_en.set_attributes(attrs)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800625 self.queue_draw()
626
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800627 def update(self, status):
628 if self._status == status:
629 return
630 self._status = status
631 label_fg = (_LABEL_UNTESTED_FG if status == TestState.UNTESTED
632 else BLACK)
Hung-Te Line94e0a02012-03-19 18:20:35 +0800633 if self._is_group:
634 self._label_en.set_text(
635 self._label_text_collapsed if status == TestState.ACTIVE
636 else self._label_text)
637
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800638 for label in [self._label_en, self._label_zh, self._label_t]:
639 label.modify_fg(gtk.STATE_NORMAL, label_fg)
640 self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[status])
641 self.queue_draw()
642
643
644class TestDirectory(gtk.VBox):
645 '''Widget containing a list of tests, colored by test status.
646
647 This is the widget corresponding to the RHS test panel.
648
649 Attributes:
650 _label_map: Dict of test path to TestLabelBox objects. Should
651 contain an entry for each test that has been visible at some
652 time.
653 _visible_status: List of (test, status) pairs reflecting the
654 last refresh of the set of visible tests. This is used to
655 rememeber what tests were active, to allow implementation of
656 visual refresh only when new active tests appear.
657 _shortcut_map: Dict of keyboard shortcut key to test path.
658 Tracks the current set of keyboard shortcut mappings for the
659 visible set of tests. This will change when the visible
660 test set changes.
661 '''
662
663 def __init__(self):
664 gtk.VBox.__init__(self)
665 self.set_spacing(0)
666 self._label_map = {}
667 self._visible_status = []
668 self._shortcut_map = {}
669
670 def _get_test_label(self, test):
671 if test.path in self._label_map:
672 return self._label_map[test.path]
673 label_box = TestLabelBox(test)
674 self._label_map[test.path] = label_box
675 return label_box
676
677 def _remove_shortcut(self, path):
678 reverse_map = dict((v, k) for k, v in self._shortcut_map.items())
679 if path not in reverse_map:
680 logging.error('Removal of non-present shortcut for %s' % path)
681 return
682 shortcut = reverse_map[path]
683 del self._shortcut_map[shortcut]
684
685 def _add_shortcut(self, test):
686 shortcut = test.kbd_shortcut
687 if shortcut in self._shortcut_map:
688 logging.error('Shortcut %s already in use by %s; cannot apply to %s'
689 % (shortcut, self._shortcut_map[shortcut], test.path))
690 shortcut = None
691 if shortcut is None:
692 # Find a suitable shortcut. For groups, use numbers. For
693 # regular tests, use alpha (letters).
Jon Salz0405ab52012-03-16 15:26:52 +0800694 if test.is_group():
695 gen = (x for x in string.digits if x not in self._shortcut_map)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800696 else:
Jon Salz0405ab52012-03-16 15:26:52 +0800697 gen = (x for x in test.label_en.lower() + string.lowercase
698 if x.isalnum() and x not in self._shortcut_map)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800699 shortcut = next(gen, None)
700 if shortcut is None:
701 logging.error('Unable to find shortcut for %s' % test.path)
702 return
703 self._shortcut_map[shortcut] = test.path
704 return shortcut
705
706 def handle_xevent(self, dummy_src, dummy_cond,
707 xhandle, keycode_map, event_client):
708 for dummy_i in range(0, xhandle.pending_events()):
709 xevent = xhandle.next_event()
710 if xevent.type != X.KeyPress:
711 continue
712 keycode = xevent.detail
713 if keycode not in keycode_map:
714 logging.warning('Ignoring unknown keycode %r' % keycode)
715 continue
716 shortcut = keycode_map[keycode]
Jon Salz0405ab52012-03-16 15:26:52 +0800717
718 if (xevent.state & GLOBAL_HOT_KEY_MASK ==
719 GLOBAL_HOT_KEY_MASK):
720 event_type = GLOBAL_HOT_KEY_EVENTS.get(shortcut)
721 if event_type:
722 event_client.post_event(Event(event_type))
723 else:
724 logging.warning('Unbound global hot key %s' % key)
725 else:
726 if shortcut not in self._shortcut_map:
727 logging.warning('Ignoring unbound shortcut %r' % shortcut)
728 continue
729 test_path = self._shortcut_map[shortcut]
730 event_client.post_event(Event(Event.Type.SWITCH_TEST,
731 path=test_path))
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800732 return True
733
734 def update(self, new_test_status):
735 '''Refresh the RHS test list to show current status and active groups.
736
737 Refresh the set of visible tests only when new active tests
738 arise. This avoids visual volatility when switching between
739 tests (intervals where no test is active). Also refresh at
740 initial startup.
741
742 Args:
743 new_test_status: A list of (test, status) tuples. The tests
744 order should match how they should be displayed in the
745 directory (rhs panel).
746 '''
747 old_active = set(t for t, s in self._visible_status
748 if s == TestState.ACTIVE)
749 new_active = set(t for t, s in new_test_status
750 if s == TestState.ACTIVE)
751 new_visible = set(t for t, s in new_test_status)
752 old_visible = set(t for t, s in self._visible_status)
753
754 if old_active and not new_active - old_active:
755 # No new active tests, so do not change the displayed test
756 # set, only update the displayed status for currently
757 # visible tests. Not updating _visible_status allows us
758 # to remember the last set of active tests.
759 for test, _ in self._visible_status:
760 status = test.get_state().status
761 self._label_map[test.path].update(status)
762 return
763
764 self._visible_status = new_test_status
765
766 new_test_map = dict((t.path, t) for t, s in new_test_status)
767
768 for test in old_visible - new_visible:
769 label_box = self._label_map[test.path]
770 logging.debug('removing %s test label' % test.path)
771 self.remove(label_box)
772 self._remove_shortcut(test.path)
773
774 new_tests = new_visible - old_visible
775
776 for position, (test, status) in enumerate(new_test_status):
777 label_box = self._get_test_label(test)
778 if test in new_tests:
779 shortcut = self._add_shortcut(test)
780 label_box = self._get_test_label(test)
781 label_box.set_shortcut(shortcut)
782 logging.debug('adding %s test label (sortcut %r, pos %d)' %
783 (test.path, shortcut, position))
784 self.pack_start(label_box, False, False)
785 self.reorder_child(label_box, position)
786 label_box.update(status)
787
788 self.show_all()
789
790
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800791
792class UiState(object):
793
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800794 def __init__(self, test_widget_box, test_directory_widget, test_list):
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800795 self._test_widget_box = test_widget_box
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800796 self._test_directory_widget = test_directory_widget
797 self._test_list = test_list
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800798 self._transition_count = 0
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800799 self._active_test_label_map = None
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800800 self.update_test_state()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800801
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800802 def update_test_state(self):
803 state_map = self._test_list.get_state_map()
804 active_tests = set(
805 t for t in self._test_list.walk()
806 if t.is_leaf() and state_map[t].status == TestState.ACTIVE)
807 active_groups = set(g for t in active_tests
808 for g in t.get_ancestor_groups())
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800809
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800810 def filter_visible_test_state(tests):
811 '''List currently visible tests and their status.
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800812
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800813 Visible means currently displayed in the RHS panel.
814 Visiblity is implied by being a top level test or having
815 membership in a group with at least one active test.
816
817 Returns:
818 A list of (test, status) tuples for all visible tests,
819 in the order they should be displayed.
820 '''
821 results = []
822 for test in tests:
Jon Salz0405ab52012-03-16 15:26:52 +0800823 if test.is_group():
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800824 results.append((test, TestState.UNTESTED))
825 if test not in active_groups:
826 continue
827 results += filter_visible_test_state(test.subtests)
828 else:
829 results.append((test, state_map[test].status))
830 return results
831
832 visible_test_state = filter_visible_test_state(self._test_list.subtests)
833 self._test_directory_widget.update(visible_test_state)
834
835 def remove_state_widget():
836 for child in self._test_widget_box.get_children():
837 self._test_widget_box.remove(child)
838 self._active_test_label_map = None
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800839
840 if not active_tests:
841 # Display the "no active tests" widget if there are still no
842 # active tests after _NO_ACTIVE_TEST_DELAY_MS.
843 def run(transition_count):
844 if transition_count != self._transition_count:
845 # Something has happened
846 return False
847
848 self._transition_count += 1
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800849 remove_state_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800850
851 self._test_widget_box.set(0.5, 0.5, 0.0, 0.0)
852 self._test_widget_box.set_padding(0, 0, 0, 0)
853 label_box = gtk.EventBox()
854 label_box.modify_bg(gtk.STATE_NORMAL, BLACK)
Hung-Te Line94e0a02012-03-19 18:20:35 +0800855 label = make_label(MESSAGE_NO_ACTIVE_TESTS,
856 font=_OTHER_LABEL_FONT,
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800857 alignment=(0.5, 0.5))
858 label_box.add(label)
859 self._test_widget_box.add(label_box)
860 self._test_widget_box.show_all()
861
862 gobject.timeout_add(_NO_ACTIVE_TEST_DELAY_MS, run,
863 self._transition_count)
864 return
865
866 self._transition_count += 1
867
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800868 if any(t.has_ui for t in active_tests):
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800869 # Remove the widget (if any) since there is an active test
870 # with a UI.
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800871 remove_state_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800872 return
873
874 if (self._active_test_label_map is not None and
875 all(t in self._active_test_label_map for t in active_tests)):
876 # All active tests are already present in the summary, so just
877 # update their states.
878 for test, label in self._active_test_label_map.iteritems():
879 label.modify_fg(
880 gtk.STATE_NORMAL,
881 LABEL_COLORS[state_map[test].status])
882 return
883
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800884 remove_state_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800885 # No active UI; draw summary of current test states
886 self._test_widget_box.set(0.5, 0.0, 0.0, 0.0)
887 self._test_widget_box.set_padding(40, 0, 0, 0)
888 vbox, self._active_test_label_map = make_summary_box(
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800889 [t for t in self._test_list.subtests
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800890 if state_map[t].status == TestState.ACTIVE],
891 state_map)
892 self._test_widget_box.add(vbox)
893 self._test_widget_box.show_all()
894
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800895
896def grab_shortcut_keys(disp, event_handler, event_client):
897 # We want to receive KeyPress events
898 root = disp.screen().root
899 root.change_attributes(event_mask = X.KeyPressMask)
900 shortcut_set = set(string.lowercase + string.digits)
901 keycode_map = {}
902 for mod, shortcut in ([(X.ControlMask, k) for k in shortcut_set] +
Jon Salz0405ab52012-03-16 15:26:52 +0800903 [(GLOBAL_HOT_KEY_MASK, k)
904 for k in GLOBAL_HOT_KEY_EVENTS] +
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800905 [(X.Mod1Mask, 'Tab')]): # Mod1 = Alt
906 keysym = gtk.gdk.keyval_from_name(shortcut)
907 keycode = disp.keysym_to_keycode(keysym)
908 keycode_map[keycode] = shortcut
909 root.grab_key(keycode, mod, 1, X.GrabModeAsync, X.GrabModeAsync)
910 # This flushes the XGrabKey calls to the server.
911 for dummy_x in range(0, root.display.pending_events()):
912 root.display.next_event()
913 gobject.io_add_watch(root.display, gobject.IO_IN, event_handler,
914 root.display, keycode_map, event_client)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800915
916
917def main(test_list_path):
918 '''Starts the main UI.
919
920 This is launched by the autotest/cros/factory/client.
921 When operators press keyboard shortcuts, the shortcut
922 value is sent as an event to the control program.'''
923
924 test_list = None
925 ui_state = None
926 event_client = None
927
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800928 def handle_key_release_event(_, event):
929 logging.info('base ui key event (%s)', event.keyval)
930 return True
931
932 def handle_event(event):
933 if event.type == Event.Type.STATE_CHANGE:
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800934 ui_state.update_test_state()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800935
936 test_list = factory.read_test_list(test_list_path)
937
938 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
939 window.connect('destroy', lambda _: gtk.main_quit())
940 window.modify_bg(gtk.STATE_NORMAL, BLACK)
941
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800942 disp = Display()
943
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800944 event_client = EventClient(
945 callback=handle_event,
946 event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
947
948 screen = window.get_screen()
949 if (screen is None):
950 logging.info('ERROR: communication with the X server is not working, ' +
951 'could not find a working screen. UI exiting.')
952 sys.exit(1)
953
954 screen_size_str = os.environ.get('CROS_SCREEN_SIZE')
955 if screen_size_str:
956 match = re.match(r'^(\d+)x(\d+)$', screen_size_str)
957 assert match, 'CROS_SCREEN_SIZE should be {width}x{height}'
958 screen_size = (int(match.group(1)), int(match.group(2)))
959 else:
960 screen_size = (screen.get_width(), screen.get_height())
961 window.set_size_request(*screen_size)
962
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800963 test_directory = TestDirectory()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800964
965 rhs_box = gtk.EventBox()
966 rhs_box.modify_bg(gtk.STATE_NORMAL, _LABEL_TROUGH_COLOR)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800967 rhs_box.add(test_directory)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800968
969 console_box = gtk.EventBox()
Chun-Ta Lin734e96a2012-03-16 20:05:33 +0800970 console_box.set_size_request(*convert_pixels((-1, 180)))
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800971 console_box.modify_bg(gtk.STATE_NORMAL, BLACK)
972
973 test_widget_box = gtk.Alignment()
974 test_widget_box.set_size_request(-1, -1)
975
976 lhs_box = gtk.VBox()
977 lhs_box.pack_end(console_box, False, False)
978 lhs_box.pack_start(test_widget_box)
979 lhs_box.pack_start(make_hsep(3), False, False)
980
981 base_box = gtk.HBox()
982 base_box.pack_end(rhs_box, False, False)
983 base_box.pack_end(make_vsep(3), False, False)
984 base_box.pack_start(lhs_box)
985
986 window.connect('key-release-event', handle_key_release_event)
987 window.add_events(gtk.gdk.KEY_RELEASE_MASK)
988
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800989 ui_state = UiState(test_widget_box, test_directory, test_list)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800990
991 window.add(base_box)
992 window.show_all()
993
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800994 grab_shortcut_keys(disp, test_directory.handle_xevent, event_client)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800995
996 hide_cursor(window.window)
997
998 test_widget_allocation = test_widget_box.get_allocation()
999 test_widget_size = (test_widget_allocation.width,
1000 test_widget_allocation.height)
1001 factory.set_shared_data('test_widget_size', test_widget_size)
1002
1003 dummy_console = Console(console_box.get_allocation())
1004
1005 event_client.post_event(Event(Event.Type.UI_READY))
1006
1007 logging.info('cros/factory/ui setup done, starting gtk.main()...')
1008 gtk.main()
1009 logging.info('cros/factory/ui gtk.main() finished, exiting.')
1010
1011
1012if __name__ == '__main__':
Jon Salz14bcbb02012-03-17 15:11:50 +08001013 parser = OptionParser(usage='usage: %prog [options] TEST-LIST-PATH')
1014 parser.add_option('-v', '--verbose', dest='verbose',
1015 action='store_true',
1016 help='Enable debug logging')
1017 (options, args) = parser.parse_args()
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001018
Jon Salz14bcbb02012-03-17 15:11:50 +08001019 if len(args) != 1:
1020 parser.error('Incorrect number of arguments')
1021
1022 factory.init_logging('ui', verbose=options.verbose)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001023 main(sys.argv[1])