blob: 289e12a0c556e317a68bb947790ff1e73af322ac [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 Lin96632362012-03-20 21:14:18 +080037from itertools import count, 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)
Hsinyu Chaoe8584b22012-04-05 17:53:08 +080083RGBA_RED_OVERLAY = (0.5, 0, 0, 0.6)
Hung-Te Linf2f78f72012-02-08 19:27:11 +080084
85LABEL_COLORS = {
86 TestState.ACTIVE: gtk.gdk.color_parse('light goldenrod'),
87 TestState.PASSED: gtk.gdk.color_parse('pale green'),
88 TestState.FAILED: gtk.gdk.color_parse('tomato'),
89 TestState.UNTESTED: gtk.gdk.color_parse('dark slate grey')}
90
91LABEL_FONT = pango.FontDescription('courier new condensed 16')
Hung-Te Linbf545582012-02-15 17:08:07 +080092LABEL_LARGE_FONT = pango.FontDescription('courier new condensed 24')
Hung-Te Linf2f78f72012-02-08 19:27:11 +080093
Jon Salzf81f6082012-03-23 19:37:34 +080094FAIL_TIMEOUT = 60
Hung-Te Linf2f78f72012-02-08 19:27:11 +080095
Hung-Te Line94e0a02012-03-19 18:20:35 +080096MESSAGE_NO_ACTIVE_TESTS = (
97 "No more tests to run. To re-run items, press shortcuts\n"
98 "from the test list in right side or from following list:\n\n"
99 "Ctrl-Alt-A (Auto-Run):\n"
100 " Test remaining untested items.\n\n"
101 "Ctrl-Alt-F (Re-run Failed):\n"
Hung-Te Lin96632362012-03-20 21:14:18 +0800102 " Re-test failed items.\n\n"
Hung-Te Line94e0a02012-03-19 18:20:35 +0800103 "Ctrl-Alt-R (Reset):\n"
104 " Re-test everything.\n\n"
Hung-Te Lin96632362012-03-20 21:14:18 +0800105 "Ctrl-Alt-Z (Information):\n"
106 " Review test results and information.\n\n"
Hung-Te Line94e0a02012-03-19 18:20:35 +0800107 )
108
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800109USER_PASS_FAIL_SELECT_STR = (
110 'hit TAB to fail and ENTER to pass\n' +
111 '錯誤請按 TAB,成功請按 ENTER')
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800112# Resolution where original UI is designed for.
113_UI_SCREEN_WIDTH = 1280
114_UI_SCREEN_HEIGHT = 800
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800115
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800116_LABEL_STATUS_ROW_SIZE = (300, 30)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800117_LABEL_EN_SIZE = (170, 35)
118_LABEL_ZH_SIZE = (70, 35)
119_LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
120_LABEL_ZH_FONT = pango.FontDescription('normal 12')
121_LABEL_T_SIZE = (40, 35)
122_LABEL_T_FONT = pango.FontDescription('arial ultra-condensed 10')
123_LABEL_UNTESTED_FG = gtk.gdk.color_parse('grey40')
124_LABEL_TROUGH_COLOR = gtk.gdk.color_parse('grey20')
125_LABEL_STATUS_SIZE = (140, 30)
126_LABEL_STATUS_FONT = pango.FontDescription(
127 'courier new bold extra-condensed 16')
128_OTHER_LABEL_FONT = pango.FontDescription('courier new condensed 20')
129
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800130_NO_ACTIVE_TEST_DELAY_MS = 500
131
Jon Salz0405ab52012-03-16 15:26:52 +0800132GLOBAL_HOT_KEY_EVENTS = {
133 'r': Event.Type.RESTART_TESTS,
134 'a': Event.Type.AUTO_RUN,
135 'f': Event.Type.RE_RUN_FAILED,
Hung-Te Lin96632362012-03-20 21:14:18 +0800136 'z': Event.Type.REVIEW,
Jon Salz0405ab52012-03-16 15:26:52 +0800137 }
138try:
139 # Works only if X is available.
140 GLOBAL_HOT_KEY_MASK = X.ControlMask | X.Mod1Mask
141except:
142 pass
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800143
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800144# ---------------------------------------------------------------------------
145# Client Library
146
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800147
Hung-Te Lin4b0b61f2012-03-05 14:20:06 +0800148# TODO(hungte) Replace gtk_lock by gtk.gdk.lock when it's availble (need pygtk
149# 2.2x, and we're now pinned by 2.1x)
150class _GtkLock(object):
151 __enter__ = gtk.gdk.threads_enter
152 def __exit__(*ignored):
153 gtk.gdk.threads_leave()
154
155
156gtk_lock = _GtkLock()
157
158
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800159def make_label(message, font=LABEL_FONT, fg=LIGHT_GREEN,
160 size=None, alignment=None):
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800161 """Returns a label widget.
162
163 A wrapper for gtk.Label. The unit of size is pixels under resolution
164 _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
165
166 @param message: A string to be displayed.
167 @param font: Font descriptor for the label.
168 @param fg: Foreground color.
169 @param size: Minimum size for this label.
170 @param alignment: Alignment setting.
171 @return: A label widget.
172 """
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800173 l = gtk.Label(message)
174 l.modify_font(font)
175 l.modify_fg(gtk.STATE_NORMAL, fg)
176 if size:
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800177 # Convert size according to the current resolution.
178 l.set_size_request(*convert_pixels(size))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800179 if alignment:
180 l.set_alignment(*alignment)
181 return l
182
183
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800184def make_status_row(init_prompt,
185 init_status,
186 label_size=_LABEL_STATUS_ROW_SIZE):
187 """Returns a widget that live updates prompt and status in a row.
188
189 Args:
190 init_prompt: The prompt label text.
191 init_status: The status label text.
192 label_size: The desired size of the prompt label and the status label.
193
194 Returns:
195 1) A dict whose content is linked by the widget.
196 2) A widget to render dict content in "prompt: status" format.
197 """
198 display_dict = {}
199 display_dict['prompt'] = init_prompt
200 display_dict['status'] = init_status
201
202 def prompt_label_expose(widget, event):
203 prompt = display_dict['prompt']
204 widget.set_text(prompt)
205
206 def status_label_expose(widget, event):
207 status = display_dict['status']
208 widget.set_text(status)
209 widget.modify_fg(gtk.STATE_NORMAL, LABEL_COLORS[status])
210
211 prompt_label = make_label(
212 init_prompt, size=label_size,
213 alignment=(0, 0.5))
214 delimiter_label = make_label(':', alignment=(0, 0.5))
215 status_label = make_label(
216 init_status, size=label_size,
217 alignment=(0, 0.5), fg=LABEL_COLORS[init_status])
218
219 widget = gtk.HBox()
220 widget.pack_end(status_label, False, False)
221 widget.pack_end(delimiter_label, False, False)
222 widget.pack_end(prompt_label, False, False)
223
224 status_label.connect('expose_event', status_label_expose)
225 prompt_label.connect('expose_event', prompt_label_expose)
226 return display_dict, widget
227
228
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800229def convert_pixels(size):
230 """Converts a pair in pixel that is suitable for current resolution.
231
232 GTK takes pixels as its unit in many function calls. To maintain the
233 consistency of the UI in different resolution, a conversion is required.
234 Take current resolution and (_UI_SCREEN_WIDTH, _UI_SCREEN_HEIGHT) as
235 the original resolution, this function returns a pair of width and height
236 that is converted for current resolution.
237
238 Because pixels in negative usually indicates unspecified, no conversion
239 will be done for negative pixels.
240
241 In addition, the aspect ratio is not maintained in this function.
242
243 Usage Example:
244 width,_ = convert_pixels((20,-1))
245
246 @param size: A pair of pixels that designed under original resolution.
247 @return: A pair of pixels of (width, height) format.
248 Pixels returned are always integer.
249 """
250 return (int(float(size[0]) / _UI_SCREEN_WIDTH * gtk.gdk.screen_width()
251 if (size[0] > 0) else size[0]),
252 int(float(size[1]) / _UI_SCREEN_HEIGHT * gtk.gdk.screen_height()
253 if (size[1] > 0) else size[1]))
254
255
256def make_hsep(height=1):
257 """Returns a widget acts as a horizontal separation line.
258
259 The unit is pixels under resolution _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
260 """
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800261 frame = gtk.EventBox()
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800262 # Convert height according to the current resolution.
263 frame.set_size_request(*convert_pixels((-1, height)))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800264 frame.modify_bg(gtk.STATE_NORMAL, SEP_COLOR)
265 return frame
266
267
268def make_vsep(width=1):
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800269 """Returns a widget acts as a vertical separation line.
270
271 The unit is pixels under resolution _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
272 """
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800273 frame = gtk.EventBox()
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800274 # Convert width according to the current resolution.
275 frame.set_size_request(*convert_pixels((width, -1)))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800276 frame.modify_bg(gtk.STATE_NORMAL, SEP_COLOR)
277 return frame
278
279
Hung-Te Linaf8ec542012-03-14 20:01:40 +0800280def make_countdown_widget(prompt=None, value=None, fg=LIGHT_GREEN):
281 if prompt is None:
282 prompt = 'time remaining / 剩餘時間: '
283 if value is None:
284 value = '%s' % FAIL_TIMEOUT
285 title = make_label(prompt, fg=fg, alignment=(1, 0.5))
286 countdown = make_label(value, fg=fg, alignment=(0, 0.5))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800287 hbox = gtk.HBox()
288 hbox.pack_start(title)
289 hbox.pack_start(countdown)
290 eb = gtk.EventBox()
291 eb.modify_bg(gtk.STATE_NORMAL, BLACK)
292 eb.add(hbox)
293 return eb, countdown
294
295
Jon Salzb1b39092012-05-03 02:05:09 +0800296def is_chrome_ui():
297 return os.environ.get('CROS_UI') == 'chrome'
298
299
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800300def hide_cursor(gdk_window):
301 pixmap = gtk.gdk.Pixmap(None, 1, 1, 1)
302 color = gtk.gdk.Color()
303 cursor = gtk.gdk.Cursor(pixmap, pixmap, color, color, 0, 0)
304 gdk_window.set_cursor(cursor)
305
306
307def calc_scale(wanted_x, wanted_y):
308 (widget_size_x, widget_size_y) = factory.get_shared_data('test_widget_size')
309 scale_x = (0.9 * widget_size_x) / wanted_x
310 scale_y = (0.9 * widget_size_y) / wanted_y
311 scale = scale_y if scale_y < scale_x else scale_x
312 scale = 1 if scale > 1 else scale
313 factory.log('scale: %s' % scale)
314 return scale
315
316
317def trim(text, length):
318 if len(text) > length:
319 text = text[:length-3] + '...'
320 return text
321
322
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800323class InputError(ValueError):
324 """Execption for input window callbacks to change status text message."""
325 pass
326
327
Hung-Te Linbf545582012-02-15 17:08:07 +0800328def make_input_window(prompt=None,
329 init_value=None,
330 msg_invalid=None,
331 font=None,
332 on_validate=None,
333 on_keypress=None,
334 on_complete=None):
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800335 """Creates a widget to prompt user for a valid string.
Hung-Te Linbf545582012-02-15 17:08:07 +0800336
337 @param prompt: A string to be displayed. None for default message.
338 @param init_value: Initial value to be set.
339 @param msg_invalid: Status string to display when input is invalid. None for
340 default message.
341 @param font: Font specification (string or pango.FontDescription) for label
342 and entry. None for default large font.
343 @param on_validate: A callback function to validate if the input from user
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800344 is valid. None for allowing any non-empty input. Any ValueError or
345 ui.InputError raised during execution in on_validate will be displayed
346 in bottom status.
Hung-Te Linbf545582012-02-15 17:08:07 +0800347 @param on_keypress: A callback function when each keystroke is hit.
348 @param on_complete: A callback function when a valid string is passed.
349 None to stop (gtk.main_quit).
Hung-Te Lin7130d9e2012-03-16 15:41:18 +0800350 @return: A widget with prompt, input entry, and status label. To access
351 these elements, use attribute 'prompt', 'entry', and 'label'.
Hung-Te Linbf545582012-02-15 17:08:07 +0800352 """
353 DEFAULT_MSG_INVALID = "Invalid input / 輸入不正確"
354 DEFAULT_PROMPT = "Enter Data / 輸入資料:"
355
356 def enter_callback(entry):
357 text = entry.get_text()
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800358 try:
359 if (on_validate and (not on_validate(text))) or (not text.strip()):
360 raise ValueError(msg_invalid)
Hung-Te Linbf545582012-02-15 17:08:07 +0800361 on_complete(text) if on_complete else gtk.main_quit()
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800362 except ValueError as e:
363 gtk.gdk.beep()
364 status_label.set_text('ERROR: %s' % e.message)
Hung-Te Linbf545582012-02-15 17:08:07 +0800365 return True
366
367 def key_press_callback(entry, key):
368 status_label.set_text('')
369 if on_keypress:
370 return on_keypress(entry, key)
371 return False
372
373 # Populate default parameters
374 if msg_invalid is None:
375 msg_invalid = DEFAULT_MSG_INVALID
376
377 if prompt is None:
378 prompt = DEFAULT_PROMPT
379
380 if font is None:
381 font = LABEL_LARGE_FONT
382 elif not isinstance(font, pango.FontDescription):
383 font = pango.FontDescription(font)
384
385 widget = gtk.VBox()
386 label = make_label(prompt, font=font)
387 status_label = make_label('', font=font)
388 entry = gtk.Entry()
389 entry.modify_font(font)
390 entry.connect("activate", enter_callback)
391 entry.connect("key_press_event", key_press_callback)
392 if init_value:
393 entry.set_text(init_value)
394 widget.modify_bg(gtk.STATE_NORMAL, BLACK)
395 status_label.modify_fg(gtk.STATE_NORMAL, RED)
396 widget.add(label)
397 widget.pack_start(entry)
398 widget.pack_start(status_label)
Chun-ta Lin2e7f44c2012-03-03 07:29:36 +0800399
Hung-Te Lin7130d9e2012-03-16 15:41:18 +0800400 widget.entry = entry
401 widget.status = status_label
402 widget.prompt = label
403
404 # TODO(itspeter) Replace deprecated get_entry by widget.entry.
Chun-ta Lin2e7f44c2012-03-03 07:29:36 +0800405 # Method for getting the entry.
406 widget.get_entry = lambda : entry
Hung-Te Linbf545582012-02-15 17:08:07 +0800407 return widget
408
409
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800410def make_summary_box(tests, state_map, rows=15):
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800411 '''Creates a widget display status of a set of test.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800412
413 @param tests: A list of FactoryTest nodes whose status (and children's
414 status) should be displayed.
415 @param state_map: The state map as provide by the state instance.
416 @param rows: The number of rows to display.
417 @return: A tuple (widget, label_map), where widget is the widget, and
418 label_map is a map from each test to the corresponding label.
419 '''
420 LABEL_EN_SIZE = (170, 35)
421 LABEL_EN_SIZE_2 = (450, 25)
422 LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
423
424 all_tests = sum([list(t.walk(in_order=True)) for t in tests], [])
425 columns = len(all_tests) / rows + (len(all_tests) % rows != 0)
426
427 info_box = gtk.HBox()
428 info_box.set_spacing(20)
429 for status in (TestState.ACTIVE, TestState.PASSED,
430 TestState.FAILED, TestState.UNTESTED):
431 label = make_label(status,
432 size=LABEL_EN_SIZE,
433 font=LABEL_EN_FONT,
434 alignment=(0.5, 0.5),
435 fg=LABEL_COLORS[status])
436 info_box.pack_start(label, False, False)
437
438 vbox = gtk.VBox()
439 vbox.set_spacing(20)
440 vbox.pack_start(info_box, False, False)
441
442 label_map = {}
443
444 if all_tests:
445 status_table = gtk.Table(rows, columns, True)
446 for (j, i), t in izip(product(xrange(columns), xrange(rows)),
447 all_tests):
448 msg_en = ' ' * (t.depth() - 1) + t.label_en
449 msg_en = trim(msg_en, 12)
450 if t.label_zh:
451 msg = '{0:<12} ({1})'.format(msg_en, t.label_zh)
452 else:
453 msg = msg_en
454 status = state_map[t].status
455 status_label = make_label(msg,
456 size=LABEL_EN_SIZE_2,
457 font=LABEL_EN_FONT,
458 alignment=(0.0, 0.5),
459 fg=LABEL_COLORS[status])
460 label_map[t] = status_label
461 status_table.attach(status_label, j, j+1, i, i+1)
462 vbox.pack_start(status_table, False, False)
463
464 return vbox, label_map
465
466
467def run_test_widget(dummy_job, test_widget,
468 invisible_cursor=True,
469 window_registration_callback=None,
470 cleanup_callback=None):
471 test_widget_size = factory.get_shared_data('test_widget_size')
472
473 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
474 window.modify_bg(gtk.STATE_NORMAL, BLACK)
475 window.set_size_request(*test_widget_size)
476
Jon Salzb1b39092012-05-03 02:05:09 +0800477 test_widget_position = factory.get_shared_data('test_widget_position')
478 if test_widget_position:
479 window.move(*test_widget_position)
480
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800481 def show_window():
482 window.show()
483 window.window.raise_() # pylint: disable=E1101
Jon Salzb1b39092012-05-03 02:05:09 +0800484 if is_chrome_ui():
485 window.present()
486 else:
487 gtk.gdk.pointer_grab(window.window, confine_to=window.window)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800488 if invisible_cursor:
489 hide_cursor(window.window)
490
491 test_path = factory.get_current_test_path()
492
493 def handle_event(event):
494 if (event.type == Event.Type.STATE_CHANGE and
495 test_path and event.path == test_path and
496 event.state.visible):
497 show_window()
498
499 event_client = EventClient(
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800500 callback=handle_event, event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800501
502 align = gtk.Alignment(xalign=0.5, yalign=0.5)
503 align.add(test_widget)
504
505 window.add(align)
506 for c in window.get_children():
507 # Show all children, but not the window itself yet.
508 c.show_all()
509
510 if window_registration_callback is not None:
511 window_registration_callback(window)
512
513 # Show the window if it is the visible test, or if the test_path is not
514 # available (e.g., run directly from the command line).
515 if (not test_path) or (
516 TestState.from_dict_or_object(
517 factory.get_state_instance().get_test_state(test_path)).visible):
518 show_window()
519 else:
520 window.hide()
521
Hung-Te Linde45e9c2012-03-19 13:02:06 +0800522 # When gtk.main() is running, it ignores all uncaught exceptions, which is
523 # not preferred by most of our factory tests. To prevent writing special
524 # function raising errors, we hook top level exception handler to always
525 # leave GTK main and raise exception again.
526
527 def exception_hook(exc_type, value, traceback):
528 # Prevent re-entrant.
529 sys.excepthook = old_excepthook
530 session['exception'] = (exc_type, value, traceback)
531 gobject.idle_add(gtk.main_quit)
532 return old_excepthook(exc_type, value, traceback)
533
534 session = {}
535 old_excepthook = sys.excepthook
536 sys.excepthook = exception_hook
537
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800538 gtk.main()
539
Jon Salzb1b39092012-05-03 02:05:09 +0800540 if not is_chrome_ui():
541 gtk.gdk.pointer_ungrab()
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800542
543 if cleanup_callback is not None:
544 cleanup_callback()
545
546 del event_client
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800547
Hung-Te Linde45e9c2012-03-19 13:02:06 +0800548 sys.excepthook = old_excepthook
549 exc_info = session.get('exception')
550 if exc_info is not None:
551 logging.error(exc_info[0], exc_info=exc_info)
552 raise error.TestError(exc_info[1])
553
554
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800555
556# ---------------------------------------------------------------------------
557# Server Implementation
558
559
560class Console(object):
561 '''Display a progress log. Implemented by launching an borderless
562 xterm at a strategic location, and running tail against the log.'''
563
564 def __init__(self, allocation):
Chun-Ta Lin734e96a2012-03-16 20:05:33 +0800565 # Specify how many lines and characters per line are displayed.
566 XTERM_DISPLAY_LINES = 13
567 XTERM_DISPLAY_CHARS = 120
568 # Extra space reserved for pixels between lines.
569 XTERM_RESERVED_LINES = 3
570
571 xterm_coords = '%dx%d+%d+%d' % (XTERM_DISPLAY_CHARS,
572 XTERM_DISPLAY_LINES,
573 allocation.x,
574 allocation.y)
575 xterm_reserved_height = gtk.gdk.screen_height() - allocation.y
576 font_size = int(float(xterm_reserved_height) / (XTERM_DISPLAY_LINES +
577 XTERM_RESERVED_LINES))
578 logging.info('xterm_reserved_height = %d' % xterm_reserved_height)
579 logging.info('font_size = %d' % font_size)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800580 logging.info('xterm_coords = %s', xterm_coords)
581 xterm_opts = ('-bg black -fg lightgray -bw 0 -g %s' % xterm_coords)
Chun-Ta Lin734e96a2012-03-16 20:05:33 +0800582 xterm_cmd = (
583 ['urxvt'] + xterm_opts.split() +
584 ['-fn', 'xft:DejaVu Sans Mono:pixelsize=%s' % font_size] +
585 ['-e', 'bash'] +
586 ['-c', 'tail -f "%s"' % factory.CONSOLE_LOG_PATH])
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800587 logging.info('xterm_cmd = %s', xterm_cmd)
588 self._proc = subprocess.Popen(xterm_cmd)
589
590 def __del__(self):
591 logging.info('console_proc __del__')
592 self._proc.kill()
593
594
595class TestLabelBox(gtk.EventBox): # pylint: disable=R0904
596
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800597 def __init__(self, test):
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800598 gtk.EventBox.__init__(self)
599 self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[TestState.UNTESTED])
Hung-Te Line94e0a02012-03-19 18:20:35 +0800600 self._is_group = test.is_group()
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800601 depth = len(test.get_ancestor_groups())
Hung-Te Line94e0a02012-03-19 18:20:35 +0800602 self._label_text = ' %s%s%s' % (
603 ' ' * depth,
604 SYMBOL_RIGHT_ARROW if self._is_group else ' ',
605 test.label_en)
606 if self._is_group:
607 self._label_text_collapsed = ' %s%s%s' % (
608 ' ' * depth,
609 SYMBOL_DOWN_ARROW if self._is_group else '',
610 test.label_en)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800611 self._label_en = make_label(
Hung-Te Line94e0a02012-03-19 18:20:35 +0800612 self._label_text, size=_LABEL_EN_SIZE,
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800613 font=_LABEL_EN_FONT, alignment=(0, 0.5),
614 fg=_LABEL_UNTESTED_FG)
615 self._label_zh = make_label(
616 test.label_zh, size=_LABEL_ZH_SIZE,
617 font=_LABEL_ZH_FONT, alignment=(0.5, 0.5),
618 fg=_LABEL_UNTESTED_FG)
619 self._label_t = make_label(
620 '', size=_LABEL_T_SIZE, font=_LABEL_T_FONT,
621 alignment=(0.5, 0.5), fg=BLACK)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800622 hbox = gtk.HBox()
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800623 hbox.pack_start(self._label_en, False, False)
624 hbox.pack_start(self._label_zh, False, False)
625 hbox.pack_start(self._label_t, False, False)
626 vbox = gtk.VBox()
627 vbox.pack_start(hbox, False, False)
628 vbox.pack_start(make_hsep(), False, False)
629 self.add(vbox)
630 self._status = None
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800631
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800632 def set_shortcut(self, shortcut):
633 if shortcut is None:
634 return
Hung-Te Lin99f64ef2012-03-22 11:05:45 +0800635 self._label_t.set_text('C-%s' % shortcut.upper())
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800636 attrs = self._label_en.get_attributes() or pango.AttrList()
637 attrs.filter(lambda attr: attr.type == pango.ATTR_UNDERLINE)
638 index_hotkey = self._label_en.get_text().upper().find(shortcut.upper())
639 if index_hotkey != -1:
640 attrs.insert(pango.AttrUnderline(
641 pango.UNDERLINE_LOW, index_hotkey, index_hotkey + 1))
642 attrs.insert(pango.AttrWeight(
643 pango.WEIGHT_BOLD, index_hotkey, index_hotkey + 1))
644 self._label_en.set_attributes(attrs)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800645 self.queue_draw()
646
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800647 def update(self, status):
648 if self._status == status:
649 return
650 self._status = status
651 label_fg = (_LABEL_UNTESTED_FG if status == TestState.UNTESTED
652 else BLACK)
Hung-Te Line94e0a02012-03-19 18:20:35 +0800653 if self._is_group:
654 self._label_en.set_text(
655 self._label_text_collapsed if status == TestState.ACTIVE
656 else self._label_text)
657
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800658 for label in [self._label_en, self._label_zh, self._label_t]:
659 label.modify_fg(gtk.STATE_NORMAL, label_fg)
660 self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[status])
661 self.queue_draw()
662
663
Hung-Te Lin96632362012-03-20 21:14:18 +0800664class ReviewInformation(object):
665
666 LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
667 TAB_BORDER = 20
668
669 def __init__(self, test_list):
670 self.test_list = test_list
671
672 def make_error_tab(self, test, state):
673 msg = '%s (%s)\n%s' % (test.label_en, test.label_zh,
674 str(state.error_msg))
675 label = make_label(msg, font=self.LABEL_EN_FONT, alignment=(0.0, 0.0))
676 label.set_line_wrap(True)
677 frame = gtk.Frame()
678 frame.add(label)
679 return frame
680
681 def make_widget(self):
682 bg_color = gtk.gdk.Color(0x1000, 0x1000, 0x1000)
683 self.notebook = gtk.Notebook()
684 self.notebook.modify_bg(gtk.STATE_NORMAL, bg_color)
685
686 test_list = self.test_list
687 state_map = test_list.get_state_map()
688 tab, _ = make_summary_box([test_list], state_map)
689 tab.set_border_width(self.TAB_BORDER)
690 self.notebook.append_page(tab, make_label('Summary'))
691
692 for i, t in izip(
693 count(1),
694 [t for t in test_list.walk()
695 if state_map[t].status == factory.TestState.FAILED
696 and t.is_leaf()]):
697 tab = self.make_error_tab(t, state_map[t])
698 tab.set_border_width(self.TAB_BORDER)
699 self.notebook.append_page(tab, make_label('#%02d' % i))
700
701 prompt = 'Review: Test Status Information'
702 if self.notebook.get_n_pages() > 1:
703 prompt += '\nPress left/right to change tabs'
704
705 control_label = make_label(prompt, font=self.LABEL_EN_FONT,
706 alignment=(0.5, 0.5))
707 vbox = gtk.VBox()
708 vbox.set_spacing(self.TAB_BORDER)
709 vbox.pack_start(control_label, False, False)
710 vbox.pack_start(self.notebook, False, False)
711 vbox.show_all()
712 vbox.grab_focus = self.notebook.grab_focus
713 return vbox
714
715
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800716class TestDirectory(gtk.VBox):
717 '''Widget containing a list of tests, colored by test status.
718
719 This is the widget corresponding to the RHS test panel.
720
721 Attributes:
722 _label_map: Dict of test path to TestLabelBox objects. Should
723 contain an entry for each test that has been visible at some
724 time.
725 _visible_status: List of (test, status) pairs reflecting the
726 last refresh of the set of visible tests. This is used to
727 rememeber what tests were active, to allow implementation of
728 visual refresh only when new active tests appear.
729 _shortcut_map: Dict of keyboard shortcut key to test path.
730 Tracks the current set of keyboard shortcut mappings for the
731 visible set of tests. This will change when the visible
732 test set changes.
733 '''
734
Tammo Spalinkddae4df2012-03-22 11:50:00 +0800735 def __init__(self, test_list):
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800736 gtk.VBox.__init__(self)
737 self.set_spacing(0)
738 self._label_map = {}
739 self._visible_status = []
740 self._shortcut_map = {}
Tammo Spalinkddae4df2012-03-22 11:50:00 +0800741 self._hard_shortcuts = set(
742 test.kbd_shortcut for test in test_list.walk()
743 if test.kbd_shortcut is not None)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800744
745 def _get_test_label(self, test):
746 if test.path in self._label_map:
747 return self._label_map[test.path]
748 label_box = TestLabelBox(test)
749 self._label_map[test.path] = label_box
750 return label_box
751
752 def _remove_shortcut(self, path):
753 reverse_map = dict((v, k) for k, v in self._shortcut_map.items())
754 if path not in reverse_map:
755 logging.error('Removal of non-present shortcut for %s' % path)
756 return
757 shortcut = reverse_map[path]
758 del self._shortcut_map[shortcut]
759
760 def _add_shortcut(self, test):
761 shortcut = test.kbd_shortcut
762 if shortcut in self._shortcut_map:
763 logging.error('Shortcut %s already in use by %s; cannot apply to %s'
764 % (shortcut, self._shortcut_map[shortcut], test.path))
765 shortcut = None
766 if shortcut is None:
767 # Find a suitable shortcut. For groups, use numbers. For
768 # regular tests, use alpha (letters).
Jon Salz0405ab52012-03-16 15:26:52 +0800769 if test.is_group():
770 gen = (x for x in string.digits if x not in self._shortcut_map)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800771 else:
Jon Salz0405ab52012-03-16 15:26:52 +0800772 gen = (x for x in test.label_en.lower() + string.lowercase
Tammo Spalinkddae4df2012-03-22 11:50:00 +0800773 if x.isalnum() and x not in self._shortcut_map
774 and x not in self._hard_shortcuts)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800775 shortcut = next(gen, None)
776 if shortcut is None:
777 logging.error('Unable to find shortcut for %s' % test.path)
778 return
779 self._shortcut_map[shortcut] = test.path
780 return shortcut
781
782 def handle_xevent(self, dummy_src, dummy_cond,
783 xhandle, keycode_map, event_client):
784 for dummy_i in range(0, xhandle.pending_events()):
785 xevent = xhandle.next_event()
786 if xevent.type != X.KeyPress:
787 continue
788 keycode = xevent.detail
789 if keycode not in keycode_map:
790 logging.warning('Ignoring unknown keycode %r' % keycode)
791 continue
792 shortcut = keycode_map[keycode]
Jon Salz0405ab52012-03-16 15:26:52 +0800793
Hung-Te Lin96632362012-03-20 21:14:18 +0800794 if (xevent.state & GLOBAL_HOT_KEY_MASK == GLOBAL_HOT_KEY_MASK):
Jon Salz0405ab52012-03-16 15:26:52 +0800795 event_type = GLOBAL_HOT_KEY_EVENTS.get(shortcut)
796 if event_type:
797 event_client.post_event(Event(event_type))
798 else:
Jon Salz968e90b2012-03-18 16:12:43 +0800799 logging.warning('Unbound global hot key %s', key)
Jon Salz0405ab52012-03-16 15:26:52 +0800800 else:
801 if shortcut not in self._shortcut_map:
802 logging.warning('Ignoring unbound shortcut %r' % shortcut)
803 continue
804 test_path = self._shortcut_map[shortcut]
805 event_client.post_event(Event(Event.Type.SWITCH_TEST,
806 path=test_path))
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800807 return True
808
809 def update(self, new_test_status):
810 '''Refresh the RHS test list to show current status and active groups.
811
812 Refresh the set of visible tests only when new active tests
813 arise. This avoids visual volatility when switching between
814 tests (intervals where no test is active). Also refresh at
815 initial startup.
816
817 Args:
818 new_test_status: A list of (test, status) tuples. The tests
819 order should match how they should be displayed in the
820 directory (rhs panel).
821 '''
822 old_active = set(t for t, s in self._visible_status
823 if s == TestState.ACTIVE)
824 new_active = set(t for t, s in new_test_status
825 if s == TestState.ACTIVE)
826 new_visible = set(t for t, s in new_test_status)
827 old_visible = set(t for t, s in self._visible_status)
828
829 if old_active and not new_active - old_active:
830 # No new active tests, so do not change the displayed test
831 # set, only update the displayed status for currently
832 # visible tests. Not updating _visible_status allows us
833 # to remember the last set of active tests.
834 for test, _ in self._visible_status:
835 status = test.get_state().status
836 self._label_map[test.path].update(status)
837 return
838
839 self._visible_status = new_test_status
840
841 new_test_map = dict((t.path, t) for t, s in new_test_status)
842
843 for test in old_visible - new_visible:
844 label_box = self._label_map[test.path]
845 logging.debug('removing %s test label' % test.path)
846 self.remove(label_box)
847 self._remove_shortcut(test.path)
848
849 new_tests = new_visible - old_visible
850
851 for position, (test, status) in enumerate(new_test_status):
852 label_box = self._get_test_label(test)
853 if test in new_tests:
854 shortcut = self._add_shortcut(test)
855 label_box = self._get_test_label(test)
856 label_box.set_shortcut(shortcut)
857 logging.debug('adding %s test label (sortcut %r, pos %d)' %
858 (test.path, shortcut, position))
859 self.pack_start(label_box, False, False)
860 self.reorder_child(label_box, position)
861 label_box.update(status)
862
863 self.show_all()
864
865
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800866
867class UiState(object):
868
Hung-Te Lin96632362012-03-20 21:14:18 +0800869 WIDGET_NONE = 0
870 WIDGET_IDLE = 1
871 WIDGET_SUMMARY = 2
872 WIDGET_REVIEW = 3
873
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800874 def __init__(self, test_widget_box, test_directory_widget, test_list):
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800875 self._test_widget_box = test_widget_box
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800876 self._test_directory_widget = test_directory_widget
877 self._test_list = test_list
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800878 self._transition_count = 0
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800879 self._active_test_label_map = None
Hung-Te Lin96632362012-03-20 21:14:18 +0800880 self._active_widget = self.WIDGET_NONE
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800881 self.update_test_state()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800882
Hung-Te Lin96632362012-03-20 21:14:18 +0800883 def show_idle_widget(self):
884 self.remove_state_widget()
885 self._test_widget_box.set(0.5, 0.5, 0.0, 0.0)
886 self._test_widget_box.set_padding(0, 0, 0, 0)
887 label = make_label(MESSAGE_NO_ACTIVE_TESTS,
888 font=_OTHER_LABEL_FONT,
889 alignment=(0.5, 0.5))
890 self._test_widget_box.add(label)
891 self._test_widget_box.show_all()
892 self._active_widget = self.WIDGET_IDLE
893
894 def show_summary_widget(self):
895 self.remove_state_widget()
896 state_map = self._test_list.get_state_map()
897 self._test_widget_box.set(0.5, 0.0, 0.0, 0.0)
898 self._test_widget_box.set_padding(40, 0, 0, 0)
899 vbox, self._active_test_label_map = make_summary_box(
900 [t for t in self._test_list.subtests
901 if state_map[t].status == TestState.ACTIVE],
902 state_map)
903 self._test_widget_box.add(vbox)
904 self._test_widget_box.show_all()
905 self._active_widget = self.WIDGET_SUMMARY
906
907 def show_review_widget(self):
908 self.remove_state_widget()
909 self._review_request = False
910 self._test_widget_box.set(0.5, 0.5, 0.0, 0.0)
911 self._test_widget_box.set_padding(0, 0, 0, 0)
912 widget = ReviewInformation(self._test_list).make_widget()
913 self._test_widget_box.add(widget)
914 self._test_widget_box.show_all()
915 widget.grab_focus()
916 self._active_widget = self.WIDGET_REVIEW
917
918 def remove_state_widget(self):
919 for child in self._test_widget_box.get_children():
920 child.hide()
921 self._test_widget_box.remove(child)
922 self._active_test_label_map = None
923 self._active_widget = self.WIDGET_NONE
924
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800925 def update_test_state(self):
926 state_map = self._test_list.get_state_map()
927 active_tests = set(
928 t for t in self._test_list.walk()
929 if t.is_leaf() and state_map[t].status == TestState.ACTIVE)
930 active_groups = set(g for t in active_tests
931 for g in t.get_ancestor_groups())
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800932
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800933 def filter_visible_test_state(tests):
934 '''List currently visible tests and their status.
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800935
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800936 Visible means currently displayed in the RHS panel.
937 Visiblity is implied by being a top level test or having
938 membership in a group with at least one active test.
939
940 Returns:
941 A list of (test, status) tuples for all visible tests,
942 in the order they should be displayed.
943 '''
944 results = []
945 for test in tests:
Jon Salz0405ab52012-03-16 15:26:52 +0800946 if test.is_group():
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800947 results.append((test, TestState.UNTESTED))
948 if test not in active_groups:
949 continue
950 results += filter_visible_test_state(test.subtests)
951 else:
952 results.append((test, state_map[test].status))
953 return results
954
955 visible_test_state = filter_visible_test_state(self._test_list.subtests)
956 self._test_directory_widget.update(visible_test_state)
957
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800958 if not active_tests:
Hung-Te Lin96632362012-03-20 21:14:18 +0800959 # Display the idle or review information screen.
960 def waiting_for_transition():
961 return (self._active_widget not in
962 [self.WIDGET_REVIEW, self.WIDGET_IDLE])
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800963
Hung-Te Lin96632362012-03-20 21:14:18 +0800964 # For smooth transition between tests, idle widget if activated only
965 # after _NO_ACTIVE_TEST_DELAY_MS without state change.
966 def idle_transition_check(cookie):
967 if (waiting_for_transition() and
968 cookie == self._transition_count):
969 self._transition_count += 1
970 self.show_idle_widget()
971 return False
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800972
Hung-Te Lin96632362012-03-20 21:14:18 +0800973 if waiting_for_transition():
974 gobject.timeout_add(_NO_ACTIVE_TEST_DELAY_MS,
975 idle_transition_check,
976 self._transition_count)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800977 return
978
979 self._transition_count += 1
980
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800981 if any(t.has_ui for t in active_tests):
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800982 # Remove the widget (if any) since there is an active test
983 # with a UI.
Hung-Te Lin96632362012-03-20 21:14:18 +0800984 self.remove_state_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800985 return
986
987 if (self._active_test_label_map is not None and
988 all(t in self._active_test_label_map for t in active_tests)):
989 # All active tests are already present in the summary, so just
990 # update their states.
991 for test, label in self._active_test_label_map.iteritems():
992 label.modify_fg(
993 gtk.STATE_NORMAL,
994 LABEL_COLORS[state_map[test].status])
995 return
996
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800997 # No active UI; draw summary of current test states
Hung-Te Lin96632362012-03-20 21:14:18 +0800998 self.show_summary_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800999
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001000
1001def grab_shortcut_keys(disp, event_handler, event_client):
1002 # We want to receive KeyPress events
1003 root = disp.screen().root
1004 root.change_attributes(event_mask = X.KeyPressMask)
1005 shortcut_set = set(string.lowercase + string.digits)
1006 keycode_map = {}
1007 for mod, shortcut in ([(X.ControlMask, k) for k in shortcut_set] +
Jon Salz0405ab52012-03-16 15:26:52 +08001008 [(GLOBAL_HOT_KEY_MASK, k)
1009 for k in GLOBAL_HOT_KEY_EVENTS] +
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001010 [(X.Mod1Mask, 'Tab')]): # Mod1 = Alt
1011 keysym = gtk.gdk.keyval_from_name(shortcut)
1012 keycode = disp.keysym_to_keycode(keysym)
1013 keycode_map[keycode] = shortcut
1014 root.grab_key(keycode, mod, 1, X.GrabModeAsync, X.GrabModeAsync)
1015 # This flushes the XGrabKey calls to the server.
1016 for dummy_x in range(0, root.display.pending_events()):
1017 root.display.next_event()
1018 gobject.io_add_watch(root.display, gobject.IO_IN, event_handler,
1019 root.display, keycode_map, event_client)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001020
1021
1022def main(test_list_path):
1023 '''Starts the main UI.
1024
1025 This is launched by the autotest/cros/factory/client.
1026 When operators press keyboard shortcuts, the shortcut
1027 value is sent as an event to the control program.'''
1028
1029 test_list = None
1030 ui_state = None
1031 event_client = None
1032
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001033 def handle_key_release_event(_, event):
1034 logging.info('base ui key event (%s)', event.keyval)
1035 return True
1036
1037 def handle_event(event):
1038 if event.type == Event.Type.STATE_CHANGE:
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001039 ui_state.update_test_state()
Hung-Te Lin96632362012-03-20 21:14:18 +08001040 elif event.type == Event.Type.REVIEW:
1041 logging.info("Operator activates review information screen")
1042 ui_state.show_review_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001043
1044 test_list = factory.read_test_list(test_list_path)
1045
1046 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
1047 window.connect('destroy', lambda _: gtk.main_quit())
1048 window.modify_bg(gtk.STATE_NORMAL, BLACK)
1049
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001050 disp = Display()
1051
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001052 event_client = EventClient(
1053 callback=handle_event,
1054 event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
1055
1056 screen = window.get_screen()
1057 if (screen is None):
1058 logging.info('ERROR: communication with the X server is not working, ' +
1059 'could not find a working screen. UI exiting.')
1060 sys.exit(1)
1061
1062 screen_size_str = os.environ.get('CROS_SCREEN_SIZE')
1063 if screen_size_str:
1064 match = re.match(r'^(\d+)x(\d+)$', screen_size_str)
1065 assert match, 'CROS_SCREEN_SIZE should be {width}x{height}'
1066 screen_size = (int(match.group(1)), int(match.group(2)))
1067 else:
1068 screen_size = (screen.get_width(), screen.get_height())
1069 window.set_size_request(*screen_size)
1070
Tammo Spalinkddae4df2012-03-22 11:50:00 +08001071 test_directory = TestDirectory(test_list)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001072
1073 rhs_box = gtk.EventBox()
1074 rhs_box.modify_bg(gtk.STATE_NORMAL, _LABEL_TROUGH_COLOR)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001075 rhs_box.add(test_directory)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001076
1077 console_box = gtk.EventBox()
Chun-Ta Lin734e96a2012-03-16 20:05:33 +08001078 console_box.set_size_request(*convert_pixels((-1, 180)))
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001079 console_box.modify_bg(gtk.STATE_NORMAL, BLACK)
1080
1081 test_widget_box = gtk.Alignment()
1082 test_widget_box.set_size_request(-1, -1)
1083
1084 lhs_box = gtk.VBox()
1085 lhs_box.pack_end(console_box, False, False)
1086 lhs_box.pack_start(test_widget_box)
1087 lhs_box.pack_start(make_hsep(3), False, False)
1088
1089 base_box = gtk.HBox()
1090 base_box.pack_end(rhs_box, False, False)
1091 base_box.pack_end(make_vsep(3), False, False)
1092 base_box.pack_start(lhs_box)
1093
1094 window.connect('key-release-event', handle_key_release_event)
1095 window.add_events(gtk.gdk.KEY_RELEASE_MASK)
1096
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001097 ui_state = UiState(test_widget_box, test_directory, test_list)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001098
1099 window.add(base_box)
1100 window.show_all()
1101
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001102 grab_shortcut_keys(disp, test_directory.handle_xevent, event_client)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001103
1104 hide_cursor(window.window)
1105
1106 test_widget_allocation = test_widget_box.get_allocation()
1107 test_widget_size = (test_widget_allocation.width,
1108 test_widget_allocation.height)
1109 factory.set_shared_data('test_widget_size', test_widget_size)
1110
Jon Salz758e6cc2012-04-03 15:47:07 +08001111 if not factory.in_chroot():
1112 dummy_console = Console(console_box.get_allocation())
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001113
1114 event_client.post_event(Event(Event.Type.UI_READY))
1115
1116 logging.info('cros/factory/ui setup done, starting gtk.main()...')
1117 gtk.main()
1118 logging.info('cros/factory/ui gtk.main() finished, exiting.')
1119
1120
1121if __name__ == '__main__':
Jon Salz14bcbb02012-03-17 15:11:50 +08001122 parser = OptionParser(usage='usage: %prog [options] TEST-LIST-PATH')
1123 parser.add_option('-v', '--verbose', dest='verbose',
1124 action='store_true',
1125 help='Enable debug logging')
1126 (options, args) = parser.parse_args()
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001127
Jon Salz14bcbb02012-03-17 15:11:50 +08001128 if len(args) != 1:
1129 parser.error('Incorrect number of arguments')
1130
1131 factory.init_logging('ui', verbose=options.verbose)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001132 main(sys.argv[1])