blob: 5628971a98fc7866969c7061841fed83c35ea6b4 [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,
Chun-Ta Linc482a5e2012-05-09 16:56:07 +0800186 label_size=_LABEL_STATUS_ROW_SIZE,
187 is_standard_status=True):
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800188 """Returns a widget that live updates prompt and status in a row.
189
190 Args:
191 init_prompt: The prompt label text.
192 init_status: The status label text.
193 label_size: The desired size of the prompt label and the status label.
Chun-Ta Linc482a5e2012-05-09 16:56:07 +0800194 is_standard_status: True to interpret status by the values defined by
195 LABEL_COLORS, and render text by corresponding color. False to
196 display arbitrary text without changing text color.
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800197
198 Returns:
199 1) A dict whose content is linked by the widget.
200 2) A widget to render dict content in "prompt: status" format.
201 """
202 display_dict = {}
203 display_dict['prompt'] = init_prompt
204 display_dict['status'] = init_status
Chun-Ta Linc482a5e2012-05-09 16:56:07 +0800205 display_dict['is_standard_status'] = is_standard_status
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800206
207 def prompt_label_expose(widget, event):
208 prompt = display_dict['prompt']
209 widget.set_text(prompt)
210
211 def status_label_expose(widget, event):
212 status = display_dict['status']
213 widget.set_text(status)
Chun-Ta Linc482a5e2012-05-09 16:56:07 +0800214 if is_standard_status:
215 widget.modify_fg(gtk.STATE_NORMAL, LABEL_COLORS[status])
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800216
217 prompt_label = make_label(
218 init_prompt, size=label_size,
219 alignment=(0, 0.5))
220 delimiter_label = make_label(':', alignment=(0, 0.5))
221 status_label = make_label(
222 init_status, size=label_size,
Chun-Ta Linc482a5e2012-05-09 16:56:07 +0800223 alignment=(0, 0.5))
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800224
225 widget = gtk.HBox()
226 widget.pack_end(status_label, False, False)
227 widget.pack_end(delimiter_label, False, False)
228 widget.pack_end(prompt_label, False, False)
229
230 status_label.connect('expose_event', status_label_expose)
231 prompt_label.connect('expose_event', prompt_label_expose)
232 return display_dict, widget
233
234
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800235def convert_pixels(size):
236 """Converts a pair in pixel that is suitable for current resolution.
237
238 GTK takes pixels as its unit in many function calls. To maintain the
239 consistency of the UI in different resolution, a conversion is required.
240 Take current resolution and (_UI_SCREEN_WIDTH, _UI_SCREEN_HEIGHT) as
241 the original resolution, this function returns a pair of width and height
242 that is converted for current resolution.
243
244 Because pixels in negative usually indicates unspecified, no conversion
245 will be done for negative pixels.
246
247 In addition, the aspect ratio is not maintained in this function.
248
249 Usage Example:
250 width,_ = convert_pixels((20,-1))
251
252 @param size: A pair of pixels that designed under original resolution.
253 @return: A pair of pixels of (width, height) format.
254 Pixels returned are always integer.
255 """
256 return (int(float(size[0]) / _UI_SCREEN_WIDTH * gtk.gdk.screen_width()
257 if (size[0] > 0) else size[0]),
258 int(float(size[1]) / _UI_SCREEN_HEIGHT * gtk.gdk.screen_height()
259 if (size[1] > 0) else size[1]))
260
261
262def make_hsep(height=1):
263 """Returns a widget acts as a horizontal separation line.
264
265 The unit is pixels under resolution _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
266 """
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800267 frame = gtk.EventBox()
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800268 # Convert height according to the current resolution.
269 frame.set_size_request(*convert_pixels((-1, height)))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800270 frame.modify_bg(gtk.STATE_NORMAL, SEP_COLOR)
271 return frame
272
273
274def make_vsep(width=1):
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800275 """Returns a widget acts as a vertical separation line.
276
277 The unit is pixels under resolution _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
278 """
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800279 frame = gtk.EventBox()
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800280 # Convert width according to the current resolution.
281 frame.set_size_request(*convert_pixels((width, -1)))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800282 frame.modify_bg(gtk.STATE_NORMAL, SEP_COLOR)
283 return frame
284
285
Hung-Te Linaf8ec542012-03-14 20:01:40 +0800286def make_countdown_widget(prompt=None, value=None, fg=LIGHT_GREEN):
287 if prompt is None:
288 prompt = 'time remaining / 剩餘時間: '
289 if value is None:
290 value = '%s' % FAIL_TIMEOUT
291 title = make_label(prompt, fg=fg, alignment=(1, 0.5))
292 countdown = make_label(value, fg=fg, alignment=(0, 0.5))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800293 hbox = gtk.HBox()
294 hbox.pack_start(title)
295 hbox.pack_start(countdown)
296 eb = gtk.EventBox()
297 eb.modify_bg(gtk.STATE_NORMAL, BLACK)
298 eb.add(hbox)
299 return eb, countdown
300
301
Jon Salzb1b39092012-05-03 02:05:09 +0800302def is_chrome_ui():
303 return os.environ.get('CROS_UI') == 'chrome'
304
305
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800306def hide_cursor(gdk_window):
307 pixmap = gtk.gdk.Pixmap(None, 1, 1, 1)
308 color = gtk.gdk.Color()
309 cursor = gtk.gdk.Cursor(pixmap, pixmap, color, color, 0, 0)
310 gdk_window.set_cursor(cursor)
311
312
313def calc_scale(wanted_x, wanted_y):
314 (widget_size_x, widget_size_y) = factory.get_shared_data('test_widget_size')
315 scale_x = (0.9 * widget_size_x) / wanted_x
316 scale_y = (0.9 * widget_size_y) / wanted_y
317 scale = scale_y if scale_y < scale_x else scale_x
318 scale = 1 if scale > 1 else scale
319 factory.log('scale: %s' % scale)
320 return scale
321
322
323def trim(text, length):
324 if len(text) > length:
325 text = text[:length-3] + '...'
326 return text
327
328
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800329class InputError(ValueError):
330 """Execption for input window callbacks to change status text message."""
331 pass
332
333
Hung-Te Linbf545582012-02-15 17:08:07 +0800334def make_input_window(prompt=None,
335 init_value=None,
336 msg_invalid=None,
337 font=None,
338 on_validate=None,
339 on_keypress=None,
340 on_complete=None):
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800341 """Creates a widget to prompt user for a valid string.
Hung-Te Linbf545582012-02-15 17:08:07 +0800342
343 @param prompt: A string to be displayed. None for default message.
344 @param init_value: Initial value to be set.
345 @param msg_invalid: Status string to display when input is invalid. None for
346 default message.
347 @param font: Font specification (string or pango.FontDescription) for label
348 and entry. None for default large font.
349 @param on_validate: A callback function to validate if the input from user
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800350 is valid. None for allowing any non-empty input. Any ValueError or
351 ui.InputError raised during execution in on_validate will be displayed
352 in bottom status.
Hung-Te Linbf545582012-02-15 17:08:07 +0800353 @param on_keypress: A callback function when each keystroke is hit.
354 @param on_complete: A callback function when a valid string is passed.
355 None to stop (gtk.main_quit).
Hung-Te Lin7130d9e2012-03-16 15:41:18 +0800356 @return: A widget with prompt, input entry, and status label. To access
357 these elements, use attribute 'prompt', 'entry', and 'label'.
Hung-Te Linbf545582012-02-15 17:08:07 +0800358 """
359 DEFAULT_MSG_INVALID = "Invalid input / 輸入不正確"
360 DEFAULT_PROMPT = "Enter Data / 輸入資料:"
361
362 def enter_callback(entry):
363 text = entry.get_text()
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800364 try:
365 if (on_validate and (not on_validate(text))) or (not text.strip()):
366 raise ValueError(msg_invalid)
Hung-Te Linbf545582012-02-15 17:08:07 +0800367 on_complete(text) if on_complete else gtk.main_quit()
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800368 except ValueError as e:
369 gtk.gdk.beep()
370 status_label.set_text('ERROR: %s' % e.message)
Hung-Te Linbf545582012-02-15 17:08:07 +0800371 return True
372
373 def key_press_callback(entry, key):
374 status_label.set_text('')
375 if on_keypress:
376 return on_keypress(entry, key)
377 return False
378
379 # Populate default parameters
380 if msg_invalid is None:
381 msg_invalid = DEFAULT_MSG_INVALID
382
383 if prompt is None:
384 prompt = DEFAULT_PROMPT
385
386 if font is None:
387 font = LABEL_LARGE_FONT
388 elif not isinstance(font, pango.FontDescription):
389 font = pango.FontDescription(font)
390
391 widget = gtk.VBox()
392 label = make_label(prompt, font=font)
393 status_label = make_label('', font=font)
394 entry = gtk.Entry()
395 entry.modify_font(font)
396 entry.connect("activate", enter_callback)
397 entry.connect("key_press_event", key_press_callback)
398 if init_value:
399 entry.set_text(init_value)
400 widget.modify_bg(gtk.STATE_NORMAL, BLACK)
401 status_label.modify_fg(gtk.STATE_NORMAL, RED)
402 widget.add(label)
403 widget.pack_start(entry)
404 widget.pack_start(status_label)
Chun-ta Lin2e7f44c2012-03-03 07:29:36 +0800405
Hung-Te Lin7130d9e2012-03-16 15:41:18 +0800406 widget.entry = entry
407 widget.status = status_label
408 widget.prompt = label
409
410 # TODO(itspeter) Replace deprecated get_entry by widget.entry.
Chun-ta Lin2e7f44c2012-03-03 07:29:36 +0800411 # Method for getting the entry.
412 widget.get_entry = lambda : entry
Hung-Te Linbf545582012-02-15 17:08:07 +0800413 return widget
414
415
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800416def make_summary_box(tests, state_map, rows=15):
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800417 '''Creates a widget display status of a set of test.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800418
419 @param tests: A list of FactoryTest nodes whose status (and children's
420 status) should be displayed.
421 @param state_map: The state map as provide by the state instance.
422 @param rows: The number of rows to display.
423 @return: A tuple (widget, label_map), where widget is the widget, and
424 label_map is a map from each test to the corresponding label.
425 '''
426 LABEL_EN_SIZE = (170, 35)
427 LABEL_EN_SIZE_2 = (450, 25)
428 LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
429
430 all_tests = sum([list(t.walk(in_order=True)) for t in tests], [])
431 columns = len(all_tests) / rows + (len(all_tests) % rows != 0)
432
433 info_box = gtk.HBox()
434 info_box.set_spacing(20)
435 for status in (TestState.ACTIVE, TestState.PASSED,
436 TestState.FAILED, TestState.UNTESTED):
437 label = make_label(status,
438 size=LABEL_EN_SIZE,
439 font=LABEL_EN_FONT,
440 alignment=(0.5, 0.5),
441 fg=LABEL_COLORS[status])
442 info_box.pack_start(label, False, False)
443
444 vbox = gtk.VBox()
445 vbox.set_spacing(20)
446 vbox.pack_start(info_box, False, False)
447
448 label_map = {}
449
450 if all_tests:
451 status_table = gtk.Table(rows, columns, True)
452 for (j, i), t in izip(product(xrange(columns), xrange(rows)),
453 all_tests):
454 msg_en = ' ' * (t.depth() - 1) + t.label_en
455 msg_en = trim(msg_en, 12)
456 if t.label_zh:
457 msg = '{0:<12} ({1})'.format(msg_en, t.label_zh)
458 else:
459 msg = msg_en
460 status = state_map[t].status
461 status_label = make_label(msg,
462 size=LABEL_EN_SIZE_2,
463 font=LABEL_EN_FONT,
464 alignment=(0.0, 0.5),
465 fg=LABEL_COLORS[status])
466 label_map[t] = status_label
467 status_table.attach(status_label, j, j+1, i, i+1)
468 vbox.pack_start(status_table, False, False)
469
470 return vbox, label_map
471
472
473def run_test_widget(dummy_job, test_widget,
474 invisible_cursor=True,
475 window_registration_callback=None,
476 cleanup_callback=None):
477 test_widget_size = factory.get_shared_data('test_widget_size')
478
479 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
480 window.modify_bg(gtk.STATE_NORMAL, BLACK)
481 window.set_size_request(*test_widget_size)
482
Jon Salzb1b39092012-05-03 02:05:09 +0800483 test_widget_position = factory.get_shared_data('test_widget_position')
484 if test_widget_position:
485 window.move(*test_widget_position)
486
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800487 def show_window():
488 window.show()
489 window.window.raise_() # pylint: disable=E1101
Jon Salzb1b39092012-05-03 02:05:09 +0800490 if is_chrome_ui():
491 window.present()
Jon Salzebb2fb72012-05-21 16:04:58 +0800492 window.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
Jon Salzb1b39092012-05-03 02:05:09 +0800493 else:
494 gtk.gdk.pointer_grab(window.window, confine_to=window.window)
Jon Salzebb2fb72012-05-21 16:04:58 +0800495 if invisible_cursor:
496 hide_cursor(window.window)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800497
498 test_path = factory.get_current_test_path()
499
500 def handle_event(event):
501 if (event.type == Event.Type.STATE_CHANGE and
502 test_path and event.path == test_path and
503 event.state.visible):
504 show_window()
505
506 event_client = EventClient(
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800507 callback=handle_event, event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800508
509 align = gtk.Alignment(xalign=0.5, yalign=0.5)
510 align.add(test_widget)
511
512 window.add(align)
513 for c in window.get_children():
514 # Show all children, but not the window itself yet.
515 c.show_all()
516
517 if window_registration_callback is not None:
518 window_registration_callback(window)
519
520 # Show the window if it is the visible test, or if the test_path is not
521 # available (e.g., run directly from the command line).
522 if (not test_path) or (
523 TestState.from_dict_or_object(
524 factory.get_state_instance().get_test_state(test_path)).visible):
525 show_window()
526 else:
527 window.hide()
528
Hung-Te Linde45e9c2012-03-19 13:02:06 +0800529 # When gtk.main() is running, it ignores all uncaught exceptions, which is
530 # not preferred by most of our factory tests. To prevent writing special
531 # function raising errors, we hook top level exception handler to always
532 # leave GTK main and raise exception again.
533
534 def exception_hook(exc_type, value, traceback):
535 # Prevent re-entrant.
536 sys.excepthook = old_excepthook
537 session['exception'] = (exc_type, value, traceback)
538 gobject.idle_add(gtk.main_quit)
539 return old_excepthook(exc_type, value, traceback)
540
541 session = {}
542 old_excepthook = sys.excepthook
543 sys.excepthook = exception_hook
544
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800545 gtk.main()
546
Jon Salzb1b39092012-05-03 02:05:09 +0800547 if not is_chrome_ui():
548 gtk.gdk.pointer_ungrab()
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800549
550 if cleanup_callback is not None:
551 cleanup_callback()
552
553 del event_client
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800554
Hung-Te Linde45e9c2012-03-19 13:02:06 +0800555 sys.excepthook = old_excepthook
556 exc_info = session.get('exception')
557 if exc_info is not None:
558 logging.error(exc_info[0], exc_info=exc_info)
559 raise error.TestError(exc_info[1])
560
561
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800562
563# ---------------------------------------------------------------------------
564# Server Implementation
565
566
567class Console(object):
568 '''Display a progress log. Implemented by launching an borderless
569 xterm at a strategic location, and running tail against the log.'''
570
571 def __init__(self, allocation):
Chun-Ta Lin734e96a2012-03-16 20:05:33 +0800572 # Specify how many lines and characters per line are displayed.
573 XTERM_DISPLAY_LINES = 13
574 XTERM_DISPLAY_CHARS = 120
575 # Extra space reserved for pixels between lines.
576 XTERM_RESERVED_LINES = 3
577
578 xterm_coords = '%dx%d+%d+%d' % (XTERM_DISPLAY_CHARS,
579 XTERM_DISPLAY_LINES,
580 allocation.x,
581 allocation.y)
582 xterm_reserved_height = gtk.gdk.screen_height() - allocation.y
583 font_size = int(float(xterm_reserved_height) / (XTERM_DISPLAY_LINES +
584 XTERM_RESERVED_LINES))
585 logging.info('xterm_reserved_height = %d' % xterm_reserved_height)
586 logging.info('font_size = %d' % font_size)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800587 logging.info('xterm_coords = %s', xterm_coords)
588 xterm_opts = ('-bg black -fg lightgray -bw 0 -g %s' % xterm_coords)
Chun-Ta Lin734e96a2012-03-16 20:05:33 +0800589 xterm_cmd = (
590 ['urxvt'] + xterm_opts.split() +
591 ['-fn', 'xft:DejaVu Sans Mono:pixelsize=%s' % font_size] +
592 ['-e', 'bash'] +
593 ['-c', 'tail -f "%s"' % factory.CONSOLE_LOG_PATH])
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800594 logging.info('xterm_cmd = %s', xterm_cmd)
595 self._proc = subprocess.Popen(xterm_cmd)
596
597 def __del__(self):
598 logging.info('console_proc __del__')
599 self._proc.kill()
600
601
602class TestLabelBox(gtk.EventBox): # pylint: disable=R0904
603
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800604 def __init__(self, test):
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800605 gtk.EventBox.__init__(self)
606 self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[TestState.UNTESTED])
Hung-Te Line94e0a02012-03-19 18:20:35 +0800607 self._is_group = test.is_group()
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800608 depth = len(test.get_ancestor_groups())
Hung-Te Line94e0a02012-03-19 18:20:35 +0800609 self._label_text = ' %s%s%s' % (
610 ' ' * depth,
611 SYMBOL_RIGHT_ARROW if self._is_group else ' ',
612 test.label_en)
613 if self._is_group:
614 self._label_text_collapsed = ' %s%s%s' % (
615 ' ' * depth,
616 SYMBOL_DOWN_ARROW if self._is_group else '',
617 test.label_en)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800618 self._label_en = make_label(
Hung-Te Line94e0a02012-03-19 18:20:35 +0800619 self._label_text, size=_LABEL_EN_SIZE,
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800620 font=_LABEL_EN_FONT, alignment=(0, 0.5),
621 fg=_LABEL_UNTESTED_FG)
622 self._label_zh = make_label(
623 test.label_zh, size=_LABEL_ZH_SIZE,
624 font=_LABEL_ZH_FONT, alignment=(0.5, 0.5),
625 fg=_LABEL_UNTESTED_FG)
626 self._label_t = make_label(
627 '', size=_LABEL_T_SIZE, font=_LABEL_T_FONT,
628 alignment=(0.5, 0.5), fg=BLACK)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800629 hbox = gtk.HBox()
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800630 hbox.pack_start(self._label_en, False, False)
631 hbox.pack_start(self._label_zh, False, False)
632 hbox.pack_start(self._label_t, False, False)
633 vbox = gtk.VBox()
634 vbox.pack_start(hbox, False, False)
635 vbox.pack_start(make_hsep(), False, False)
636 self.add(vbox)
637 self._status = None
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800638
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800639 def set_shortcut(self, shortcut):
640 if shortcut is None:
641 return
Hung-Te Lin99f64ef2012-03-22 11:05:45 +0800642 self._label_t.set_text('C-%s' % shortcut.upper())
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800643 attrs = self._label_en.get_attributes() or pango.AttrList()
644 attrs.filter(lambda attr: attr.type == pango.ATTR_UNDERLINE)
645 index_hotkey = self._label_en.get_text().upper().find(shortcut.upper())
646 if index_hotkey != -1:
647 attrs.insert(pango.AttrUnderline(
648 pango.UNDERLINE_LOW, index_hotkey, index_hotkey + 1))
649 attrs.insert(pango.AttrWeight(
650 pango.WEIGHT_BOLD, index_hotkey, index_hotkey + 1))
651 self._label_en.set_attributes(attrs)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800652 self.queue_draw()
653
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800654 def update(self, status):
655 if self._status == status:
656 return
657 self._status = status
658 label_fg = (_LABEL_UNTESTED_FG if status == TestState.UNTESTED
659 else BLACK)
Hung-Te Line94e0a02012-03-19 18:20:35 +0800660 if self._is_group:
661 self._label_en.set_text(
662 self._label_text_collapsed if status == TestState.ACTIVE
663 else self._label_text)
664
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800665 for label in [self._label_en, self._label_zh, self._label_t]:
666 label.modify_fg(gtk.STATE_NORMAL, label_fg)
667 self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[status])
668 self.queue_draw()
669
670
Hung-Te Lin96632362012-03-20 21:14:18 +0800671class ReviewInformation(object):
672
673 LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
674 TAB_BORDER = 20
675
676 def __init__(self, test_list):
677 self.test_list = test_list
678
679 def make_error_tab(self, test, state):
680 msg = '%s (%s)\n%s' % (test.label_en, test.label_zh,
681 str(state.error_msg))
682 label = make_label(msg, font=self.LABEL_EN_FONT, alignment=(0.0, 0.0))
683 label.set_line_wrap(True)
684 frame = gtk.Frame()
685 frame.add(label)
686 return frame
687
688 def make_widget(self):
689 bg_color = gtk.gdk.Color(0x1000, 0x1000, 0x1000)
690 self.notebook = gtk.Notebook()
691 self.notebook.modify_bg(gtk.STATE_NORMAL, bg_color)
692
693 test_list = self.test_list
694 state_map = test_list.get_state_map()
695 tab, _ = make_summary_box([test_list], state_map)
696 tab.set_border_width(self.TAB_BORDER)
697 self.notebook.append_page(tab, make_label('Summary'))
698
699 for i, t in izip(
700 count(1),
701 [t for t in test_list.walk()
702 if state_map[t].status == factory.TestState.FAILED
703 and t.is_leaf()]):
704 tab = self.make_error_tab(t, state_map[t])
705 tab.set_border_width(self.TAB_BORDER)
706 self.notebook.append_page(tab, make_label('#%02d' % i))
707
708 prompt = 'Review: Test Status Information'
709 if self.notebook.get_n_pages() > 1:
710 prompt += '\nPress left/right to change tabs'
711
712 control_label = make_label(prompt, font=self.LABEL_EN_FONT,
713 alignment=(0.5, 0.5))
714 vbox = gtk.VBox()
715 vbox.set_spacing(self.TAB_BORDER)
716 vbox.pack_start(control_label, False, False)
717 vbox.pack_start(self.notebook, False, False)
718 vbox.show_all()
719 vbox.grab_focus = self.notebook.grab_focus
720 return vbox
721
722
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800723class TestDirectory(gtk.VBox):
724 '''Widget containing a list of tests, colored by test status.
725
726 This is the widget corresponding to the RHS test panel.
727
728 Attributes:
729 _label_map: Dict of test path to TestLabelBox objects. Should
730 contain an entry for each test that has been visible at some
731 time.
732 _visible_status: List of (test, status) pairs reflecting the
733 last refresh of the set of visible tests. This is used to
734 rememeber what tests were active, to allow implementation of
735 visual refresh only when new active tests appear.
736 _shortcut_map: Dict of keyboard shortcut key to test path.
737 Tracks the current set of keyboard shortcut mappings for the
738 visible set of tests. This will change when the visible
739 test set changes.
740 '''
741
Tammo Spalinkddae4df2012-03-22 11:50:00 +0800742 def __init__(self, test_list):
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800743 gtk.VBox.__init__(self)
744 self.set_spacing(0)
745 self._label_map = {}
746 self._visible_status = []
747 self._shortcut_map = {}
Tammo Spalinkddae4df2012-03-22 11:50:00 +0800748 self._hard_shortcuts = set(
749 test.kbd_shortcut for test in test_list.walk()
750 if test.kbd_shortcut is not None)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800751
752 def _get_test_label(self, test):
753 if test.path in self._label_map:
754 return self._label_map[test.path]
755 label_box = TestLabelBox(test)
756 self._label_map[test.path] = label_box
757 return label_box
758
759 def _remove_shortcut(self, path):
760 reverse_map = dict((v, k) for k, v in self._shortcut_map.items())
761 if path not in reverse_map:
762 logging.error('Removal of non-present shortcut for %s' % path)
763 return
764 shortcut = reverse_map[path]
765 del self._shortcut_map[shortcut]
766
767 def _add_shortcut(self, test):
768 shortcut = test.kbd_shortcut
769 if shortcut in self._shortcut_map:
770 logging.error('Shortcut %s already in use by %s; cannot apply to %s'
771 % (shortcut, self._shortcut_map[shortcut], test.path))
772 shortcut = None
773 if shortcut is None:
774 # Find a suitable shortcut. For groups, use numbers. For
775 # regular tests, use alpha (letters).
Jon Salz0405ab52012-03-16 15:26:52 +0800776 if test.is_group():
777 gen = (x for x in string.digits if x not in self._shortcut_map)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800778 else:
Jon Salz0405ab52012-03-16 15:26:52 +0800779 gen = (x for x in test.label_en.lower() + string.lowercase
Tammo Spalinkddae4df2012-03-22 11:50:00 +0800780 if x.isalnum() and x not in self._shortcut_map
781 and x not in self._hard_shortcuts)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800782 shortcut = next(gen, None)
783 if shortcut is None:
784 logging.error('Unable to find shortcut for %s' % test.path)
785 return
786 self._shortcut_map[shortcut] = test.path
787 return shortcut
788
789 def handle_xevent(self, dummy_src, dummy_cond,
790 xhandle, keycode_map, event_client):
791 for dummy_i in range(0, xhandle.pending_events()):
792 xevent = xhandle.next_event()
793 if xevent.type != X.KeyPress:
794 continue
795 keycode = xevent.detail
796 if keycode not in keycode_map:
797 logging.warning('Ignoring unknown keycode %r' % keycode)
798 continue
799 shortcut = keycode_map[keycode]
Jon Salz0405ab52012-03-16 15:26:52 +0800800
Hung-Te Lin96632362012-03-20 21:14:18 +0800801 if (xevent.state & GLOBAL_HOT_KEY_MASK == GLOBAL_HOT_KEY_MASK):
Jon Salz0405ab52012-03-16 15:26:52 +0800802 event_type = GLOBAL_HOT_KEY_EVENTS.get(shortcut)
803 if event_type:
804 event_client.post_event(Event(event_type))
805 else:
Jon Salz968e90b2012-03-18 16:12:43 +0800806 logging.warning('Unbound global hot key %s', key)
Jon Salz0405ab52012-03-16 15:26:52 +0800807 else:
808 if shortcut not in self._shortcut_map:
809 logging.warning('Ignoring unbound shortcut %r' % shortcut)
810 continue
811 test_path = self._shortcut_map[shortcut]
812 event_client.post_event(Event(Event.Type.SWITCH_TEST,
813 path=test_path))
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800814 return True
815
816 def update(self, new_test_status):
817 '''Refresh the RHS test list to show current status and active groups.
818
819 Refresh the set of visible tests only when new active tests
820 arise. This avoids visual volatility when switching between
821 tests (intervals where no test is active). Also refresh at
822 initial startup.
823
824 Args:
825 new_test_status: A list of (test, status) tuples. The tests
826 order should match how they should be displayed in the
827 directory (rhs panel).
828 '''
829 old_active = set(t for t, s in self._visible_status
830 if s == TestState.ACTIVE)
831 new_active = set(t for t, s in new_test_status
832 if s == TestState.ACTIVE)
833 new_visible = set(t for t, s in new_test_status)
834 old_visible = set(t for t, s in self._visible_status)
835
836 if old_active and not new_active - old_active:
837 # No new active tests, so do not change the displayed test
838 # set, only update the displayed status for currently
839 # visible tests. Not updating _visible_status allows us
840 # to remember the last set of active tests.
841 for test, _ in self._visible_status:
842 status = test.get_state().status
843 self._label_map[test.path].update(status)
844 return
845
846 self._visible_status = new_test_status
847
848 new_test_map = dict((t.path, t) for t, s in new_test_status)
849
850 for test in old_visible - new_visible:
851 label_box = self._label_map[test.path]
852 logging.debug('removing %s test label' % test.path)
853 self.remove(label_box)
854 self._remove_shortcut(test.path)
855
856 new_tests = new_visible - old_visible
857
858 for position, (test, status) in enumerate(new_test_status):
859 label_box = self._get_test_label(test)
860 if test in new_tests:
861 shortcut = self._add_shortcut(test)
862 label_box = self._get_test_label(test)
863 label_box.set_shortcut(shortcut)
864 logging.debug('adding %s test label (sortcut %r, pos %d)' %
865 (test.path, shortcut, position))
866 self.pack_start(label_box, False, False)
867 self.reorder_child(label_box, position)
868 label_box.update(status)
869
870 self.show_all()
871
872
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800873
874class UiState(object):
875
Hung-Te Lin96632362012-03-20 21:14:18 +0800876 WIDGET_NONE = 0
877 WIDGET_IDLE = 1
878 WIDGET_SUMMARY = 2
879 WIDGET_REVIEW = 3
880
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800881 def __init__(self, test_widget_box, test_directory_widget, test_list):
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800882 self._test_widget_box = test_widget_box
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800883 self._test_directory_widget = test_directory_widget
884 self._test_list = test_list
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800885 self._transition_count = 0
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800886 self._active_test_label_map = None
Hung-Te Lin96632362012-03-20 21:14:18 +0800887 self._active_widget = self.WIDGET_NONE
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800888 self.update_test_state()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800889
Hung-Te Lin96632362012-03-20 21:14:18 +0800890 def show_idle_widget(self):
891 self.remove_state_widget()
892 self._test_widget_box.set(0.5, 0.5, 0.0, 0.0)
893 self._test_widget_box.set_padding(0, 0, 0, 0)
894 label = make_label(MESSAGE_NO_ACTIVE_TESTS,
895 font=_OTHER_LABEL_FONT,
896 alignment=(0.5, 0.5))
897 self._test_widget_box.add(label)
898 self._test_widget_box.show_all()
899 self._active_widget = self.WIDGET_IDLE
900
901 def show_summary_widget(self):
902 self.remove_state_widget()
903 state_map = self._test_list.get_state_map()
904 self._test_widget_box.set(0.5, 0.0, 0.0, 0.0)
905 self._test_widget_box.set_padding(40, 0, 0, 0)
906 vbox, self._active_test_label_map = make_summary_box(
907 [t for t in self._test_list.subtests
908 if state_map[t].status == TestState.ACTIVE],
909 state_map)
910 self._test_widget_box.add(vbox)
911 self._test_widget_box.show_all()
912 self._active_widget = self.WIDGET_SUMMARY
913
914 def show_review_widget(self):
915 self.remove_state_widget()
916 self._review_request = False
917 self._test_widget_box.set(0.5, 0.5, 0.0, 0.0)
918 self._test_widget_box.set_padding(0, 0, 0, 0)
919 widget = ReviewInformation(self._test_list).make_widget()
920 self._test_widget_box.add(widget)
921 self._test_widget_box.show_all()
922 widget.grab_focus()
923 self._active_widget = self.WIDGET_REVIEW
924
925 def remove_state_widget(self):
926 for child in self._test_widget_box.get_children():
927 child.hide()
928 self._test_widget_box.remove(child)
929 self._active_test_label_map = None
930 self._active_widget = self.WIDGET_NONE
931
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800932 def update_test_state(self):
933 state_map = self._test_list.get_state_map()
934 active_tests = set(
935 t for t in self._test_list.walk()
936 if t.is_leaf() and state_map[t].status == TestState.ACTIVE)
937 active_groups = set(g for t in active_tests
938 for g in t.get_ancestor_groups())
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800939
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800940 def filter_visible_test_state(tests):
941 '''List currently visible tests and their status.
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800942
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800943 Visible means currently displayed in the RHS panel.
944 Visiblity is implied by being a top level test or having
945 membership in a group with at least one active test.
946
947 Returns:
948 A list of (test, status) tuples for all visible tests,
949 in the order they should be displayed.
950 '''
951 results = []
952 for test in tests:
Jon Salz0405ab52012-03-16 15:26:52 +0800953 if test.is_group():
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800954 results.append((test, TestState.UNTESTED))
955 if test not in active_groups:
956 continue
957 results += filter_visible_test_state(test.subtests)
958 else:
959 results.append((test, state_map[test].status))
960 return results
961
962 visible_test_state = filter_visible_test_state(self._test_list.subtests)
963 self._test_directory_widget.update(visible_test_state)
964
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800965 if not active_tests:
Hung-Te Lin96632362012-03-20 21:14:18 +0800966 # Display the idle or review information screen.
967 def waiting_for_transition():
968 return (self._active_widget not in
969 [self.WIDGET_REVIEW, self.WIDGET_IDLE])
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800970
Hung-Te Lin96632362012-03-20 21:14:18 +0800971 # For smooth transition between tests, idle widget if activated only
972 # after _NO_ACTIVE_TEST_DELAY_MS without state change.
973 def idle_transition_check(cookie):
974 if (waiting_for_transition() and
975 cookie == self._transition_count):
976 self._transition_count += 1
977 self.show_idle_widget()
978 return False
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800979
Hung-Te Lin96632362012-03-20 21:14:18 +0800980 if waiting_for_transition():
981 gobject.timeout_add(_NO_ACTIVE_TEST_DELAY_MS,
982 idle_transition_check,
983 self._transition_count)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800984 return
985
986 self._transition_count += 1
987
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800988 if any(t.has_ui for t in active_tests):
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800989 # Remove the widget (if any) since there is an active test
990 # with a UI.
Hung-Te Lin96632362012-03-20 21:14:18 +0800991 self.remove_state_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800992 return
993
994 if (self._active_test_label_map is not None and
995 all(t in self._active_test_label_map for t in active_tests)):
996 # All active tests are already present in the summary, so just
997 # update their states.
998 for test, label in self._active_test_label_map.iteritems():
999 label.modify_fg(
1000 gtk.STATE_NORMAL,
1001 LABEL_COLORS[state_map[test].status])
1002 return
1003
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001004 # No active UI; draw summary of current test states
Hung-Te Lin96632362012-03-20 21:14:18 +08001005 self.show_summary_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001006
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001007
1008def grab_shortcut_keys(disp, event_handler, event_client):
1009 # We want to receive KeyPress events
1010 root = disp.screen().root
1011 root.change_attributes(event_mask = X.KeyPressMask)
1012 shortcut_set = set(string.lowercase + string.digits)
1013 keycode_map = {}
1014 for mod, shortcut in ([(X.ControlMask, k) for k in shortcut_set] +
Jon Salz0405ab52012-03-16 15:26:52 +08001015 [(GLOBAL_HOT_KEY_MASK, k)
1016 for k in GLOBAL_HOT_KEY_EVENTS] +
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001017 [(X.Mod1Mask, 'Tab')]): # Mod1 = Alt
1018 keysym = gtk.gdk.keyval_from_name(shortcut)
1019 keycode = disp.keysym_to_keycode(keysym)
1020 keycode_map[keycode] = shortcut
1021 root.grab_key(keycode, mod, 1, X.GrabModeAsync, X.GrabModeAsync)
1022 # This flushes the XGrabKey calls to the server.
1023 for dummy_x in range(0, root.display.pending_events()):
1024 root.display.next_event()
1025 gobject.io_add_watch(root.display, gobject.IO_IN, event_handler,
1026 root.display, keycode_map, event_client)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001027
1028
1029def main(test_list_path):
1030 '''Starts the main UI.
1031
1032 This is launched by the autotest/cros/factory/client.
1033 When operators press keyboard shortcuts, the shortcut
1034 value is sent as an event to the control program.'''
1035
1036 test_list = None
1037 ui_state = None
1038 event_client = None
1039
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001040 def handle_key_release_event(_, event):
1041 logging.info('base ui key event (%s)', event.keyval)
1042 return True
1043
1044 def handle_event(event):
1045 if event.type == Event.Type.STATE_CHANGE:
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001046 ui_state.update_test_state()
Hung-Te Lin96632362012-03-20 21:14:18 +08001047 elif event.type == Event.Type.REVIEW:
1048 logging.info("Operator activates review information screen")
1049 ui_state.show_review_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001050
1051 test_list = factory.read_test_list(test_list_path)
1052
1053 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
1054 window.connect('destroy', lambda _: gtk.main_quit())
1055 window.modify_bg(gtk.STATE_NORMAL, BLACK)
1056
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001057 disp = Display()
1058
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001059 event_client = EventClient(
1060 callback=handle_event,
1061 event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
1062
1063 screen = window.get_screen()
1064 if (screen is None):
1065 logging.info('ERROR: communication with the X server is not working, ' +
1066 'could not find a working screen. UI exiting.')
1067 sys.exit(1)
1068
1069 screen_size_str = os.environ.get('CROS_SCREEN_SIZE')
1070 if screen_size_str:
1071 match = re.match(r'^(\d+)x(\d+)$', screen_size_str)
1072 assert match, 'CROS_SCREEN_SIZE should be {width}x{height}'
1073 screen_size = (int(match.group(1)), int(match.group(2)))
1074 else:
1075 screen_size = (screen.get_width(), screen.get_height())
1076 window.set_size_request(*screen_size)
1077
Tammo Spalinkddae4df2012-03-22 11:50:00 +08001078 test_directory = TestDirectory(test_list)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001079
1080 rhs_box = gtk.EventBox()
1081 rhs_box.modify_bg(gtk.STATE_NORMAL, _LABEL_TROUGH_COLOR)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001082 rhs_box.add(test_directory)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001083
1084 console_box = gtk.EventBox()
Chun-Ta Lin734e96a2012-03-16 20:05:33 +08001085 console_box.set_size_request(*convert_pixels((-1, 180)))
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001086 console_box.modify_bg(gtk.STATE_NORMAL, BLACK)
1087
1088 test_widget_box = gtk.Alignment()
1089 test_widget_box.set_size_request(-1, -1)
1090
1091 lhs_box = gtk.VBox()
1092 lhs_box.pack_end(console_box, False, False)
1093 lhs_box.pack_start(test_widget_box)
1094 lhs_box.pack_start(make_hsep(3), False, False)
1095
1096 base_box = gtk.HBox()
1097 base_box.pack_end(rhs_box, False, False)
1098 base_box.pack_end(make_vsep(3), False, False)
1099 base_box.pack_start(lhs_box)
1100
1101 window.connect('key-release-event', handle_key_release_event)
1102 window.add_events(gtk.gdk.KEY_RELEASE_MASK)
1103
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001104 ui_state = UiState(test_widget_box, test_directory, test_list)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001105
1106 window.add(base_box)
1107 window.show_all()
1108
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001109 grab_shortcut_keys(disp, test_directory.handle_xevent, event_client)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001110
1111 hide_cursor(window.window)
1112
1113 test_widget_allocation = test_widget_box.get_allocation()
1114 test_widget_size = (test_widget_allocation.width,
1115 test_widget_allocation.height)
1116 factory.set_shared_data('test_widget_size', test_widget_size)
1117
Jon Salz758e6cc2012-04-03 15:47:07 +08001118 if not factory.in_chroot():
1119 dummy_console = Console(console_box.get_allocation())
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001120
1121 event_client.post_event(Event(Event.Type.UI_READY))
1122
1123 logging.info('cros/factory/ui setup done, starting gtk.main()...')
1124 gtk.main()
1125 logging.info('cros/factory/ui gtk.main() finished, exiting.')
1126
1127
1128if __name__ == '__main__':
Jon Salz14bcbb02012-03-17 15:11:50 +08001129 parser = OptionParser(usage='usage: %prog [options] TEST-LIST-PATH')
1130 parser.add_option('-v', '--verbose', dest='verbose',
1131 action='store_true',
1132 help='Enable debug logging')
1133 (options, args) = parser.parse_args()
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001134
Jon Salz14bcbb02012-03-17 15:11:50 +08001135 if len(args) != 1:
1136 parser.error('Incorrect number of arguments')
1137
1138 factory.init_logging('ui', verbose=options.verbose)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001139 main(sys.argv[1])