blob: 729f18e2ba737ee2360ebc30ece420f4d80478a3 [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
Jon Salz5da61e62012-05-31 13:06:22 +080037import threading
38import time
Hung-Te Lin96632362012-03-20 21:14:18 +080039from itertools import count, izip, product
Jon Salz14bcbb02012-03-17 15:11:50 +080040from optparse import OptionParser
Hung-Te Linf2f78f72012-02-08 19:27:11 +080041
Hung-Te Lin6bb48552012-02-09 14:37:43 +080042# GTK and X modules
43import gobject
44import gtk
45import pango
46
Tammo Spalinkdb8d7112012-03-13 18:54:37 +080047# Guard loading Xlib because it is currently not available in the
48# image build process host-depends list. Failure to load in
49# production should always manifest during regular use.
50try:
51 from Xlib import X
52 from Xlib.display import Display
53except:
54 pass
55
Hung-Te Lin6bb48552012-02-09 14:37:43 +080056# Factory and autotest modules
Hung-Te Linf2f78f72012-02-08 19:27:11 +080057import factory_common
Jon Salz83591782012-06-26 11:09:58 +080058from cros.factory.test import factory
Jon Salz2f757d42012-06-27 17:06:42 +080059from cros.factory.test.factory import TestState
Jon Salz6d0f8202012-07-02 14:02:25 +080060from cros.factory.test.test_ui import FactoryTestFailure
Jon Salz2f757d42012-06-27 17:06:42 +080061from cros.factory.test.event import Event, EventClient
Hung-Te Linf2f78f72012-02-08 19:27:11 +080062
Hung-Te Lin6bb48552012-02-09 14:37:43 +080063
Hung-Te Linf2f78f72012-02-08 19:27:11 +080064# For compatibility with tests before TestState existed
65ACTIVE = TestState.ACTIVE
66PASSED = TestState.PASSED
67FAILED = TestState.FAILED
68UNTESTED = TestState.UNTESTED
69
Hung-Te Line94e0a02012-03-19 18:20:35 +080070# Arrow symbols
71SYMBOL_RIGHT_ARROW = u'\u25b8'
72SYMBOL_DOWN_ARROW = u'\u25bc'
73
Hung-Te Lin6bb48552012-02-09 14:37:43 +080074# Color definition
Hung-Te Linf2f78f72012-02-08 19:27:11 +080075BLACK = gtk.gdk.Color()
76RED = gtk.gdk.Color(0xFFFF, 0, 0)
77GREEN = gtk.gdk.Color(0, 0xFFFF, 0)
78BLUE = gtk.gdk.Color(0, 0, 0xFFFF)
79WHITE = gtk.gdk.Color(0xFFFF, 0xFFFF, 0xFFFF)
Hung-Te Linf2f78f72012-02-08 19:27:11 +080080LIGHT_GREEN = gtk.gdk.color_parse('light green')
Hung-Te Linf2f78f72012-02-08 19:27:11 +080081SEP_COLOR = gtk.gdk.color_parse('grey50')
82
83RGBA_GREEN_OVERLAY = (0, 0.5, 0, 0.6)
84RGBA_YELLOW_OVERLAY = (0.6, 0.6, 0, 0.6)
Hsinyu Chaoe8584b22012-04-05 17:53:08 +080085RGBA_RED_OVERLAY = (0.5, 0, 0, 0.6)
Hung-Te Linf2f78f72012-02-08 19:27:11 +080086
87LABEL_COLORS = {
88 TestState.ACTIVE: gtk.gdk.color_parse('light goldenrod'),
89 TestState.PASSED: gtk.gdk.color_parse('pale green'),
90 TestState.FAILED: gtk.gdk.color_parse('tomato'),
91 TestState.UNTESTED: gtk.gdk.color_parse('dark slate grey')}
92
93LABEL_FONT = pango.FontDescription('courier new condensed 16')
Hung-Te Linbf545582012-02-15 17:08:07 +080094LABEL_LARGE_FONT = pango.FontDescription('courier new condensed 24')
Hung-Te Linf2f78f72012-02-08 19:27:11 +080095
Jon Salzf81f6082012-03-23 19:37:34 +080096FAIL_TIMEOUT = 60
Hung-Te Linf2f78f72012-02-08 19:27:11 +080097
Hung-Te Line94e0a02012-03-19 18:20:35 +080098MESSAGE_NO_ACTIVE_TESTS = (
99 "No more tests to run. To re-run items, press shortcuts\n"
100 "from the test list in right side or from following list:\n\n"
101 "Ctrl-Alt-A (Auto-Run):\n"
102 " Test remaining untested items.\n\n"
103 "Ctrl-Alt-F (Re-run Failed):\n"
Hung-Te Lin96632362012-03-20 21:14:18 +0800104 " Re-test failed items.\n\n"
Hung-Te Line94e0a02012-03-19 18:20:35 +0800105 "Ctrl-Alt-R (Reset):\n"
106 " Re-test everything.\n\n"
Hung-Te Lin96632362012-03-20 21:14:18 +0800107 "Ctrl-Alt-Z (Information):\n"
108 " Review test results and information.\n\n"
Hung-Te Line94e0a02012-03-19 18:20:35 +0800109 )
110
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800111USER_PASS_FAIL_SELECT_STR = (
112 'hit TAB to fail and ENTER to pass\n' +
113 '錯誤請按 TAB,成功請按 ENTER')
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800114# Resolution where original UI is designed for.
115_UI_SCREEN_WIDTH = 1280
116_UI_SCREEN_HEIGHT = 800
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800117
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800118_LABEL_STATUS_ROW_SIZE = (300, 30)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800119_LABEL_EN_SIZE = (170, 35)
120_LABEL_ZH_SIZE = (70, 35)
121_LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
122_LABEL_ZH_FONT = pango.FontDescription('normal 12')
123_LABEL_T_SIZE = (40, 35)
124_LABEL_T_FONT = pango.FontDescription('arial ultra-condensed 10')
125_LABEL_UNTESTED_FG = gtk.gdk.color_parse('grey40')
126_LABEL_TROUGH_COLOR = gtk.gdk.color_parse('grey20')
127_LABEL_STATUS_SIZE = (140, 30)
128_LABEL_STATUS_FONT = pango.FontDescription(
129 'courier new bold extra-condensed 16')
130_OTHER_LABEL_FONT = pango.FontDescription('courier new condensed 20')
131
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800132_NO_ACTIVE_TEST_DELAY_MS = 500
133
Jon Salz0405ab52012-03-16 15:26:52 +0800134GLOBAL_HOT_KEY_EVENTS = {
135 'r': Event.Type.RESTART_TESTS,
136 'a': Event.Type.AUTO_RUN,
137 'f': Event.Type.RE_RUN_FAILED,
Hung-Te Lin96632362012-03-20 21:14:18 +0800138 'z': Event.Type.REVIEW,
Jon Salz0405ab52012-03-16 15:26:52 +0800139 }
140try:
141 # Works only if X is available.
142 GLOBAL_HOT_KEY_MASK = X.ControlMask | X.Mod1Mask
143except:
144 pass
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800145
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800146# ---------------------------------------------------------------------------
147# Client Library
148
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800149
Hung-Te Lin4b0b61f2012-03-05 14:20:06 +0800150# TODO(hungte) Replace gtk_lock by gtk.gdk.lock when it's availble (need pygtk
151# 2.2x, and we're now pinned by 2.1x)
152class _GtkLock(object):
153 __enter__ = gtk.gdk.threads_enter
154 def __exit__(*ignored):
155 gtk.gdk.threads_leave()
156
157
158gtk_lock = _GtkLock()
159
160
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800161def make_label(message, font=LABEL_FONT, fg=LIGHT_GREEN,
162 size=None, alignment=None):
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800163 """Returns a label widget.
164
165 A wrapper for gtk.Label. The unit of size is pixels under resolution
166 _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
167
168 @param message: A string to be displayed.
169 @param font: Font descriptor for the label.
170 @param fg: Foreground color.
171 @param size: Minimum size for this label.
172 @param alignment: Alignment setting.
173 @return: A label widget.
174 """
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800175 l = gtk.Label(message)
176 l.modify_font(font)
177 l.modify_fg(gtk.STATE_NORMAL, fg)
178 if size:
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800179 # Convert size according to the current resolution.
180 l.set_size_request(*convert_pixels(size))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800181 if alignment:
182 l.set_alignment(*alignment)
183 return l
184
185
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800186def make_status_row(init_prompt,
187 init_status,
Chun-Ta Linc482a5e2012-05-09 16:56:07 +0800188 label_size=_LABEL_STATUS_ROW_SIZE,
189 is_standard_status=True):
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800190 """Returns a widget that live updates prompt and status in a row.
191
192 Args:
193 init_prompt: The prompt label text.
194 init_status: The status label text.
195 label_size: The desired size of the prompt label and the status label.
Chun-Ta Linc482a5e2012-05-09 16:56:07 +0800196 is_standard_status: True to interpret status by the values defined by
197 LABEL_COLORS, and render text by corresponding color. False to
198 display arbitrary text without changing text color.
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800199
200 Returns:
201 1) A dict whose content is linked by the widget.
202 2) A widget to render dict content in "prompt: status" format.
203 """
204 display_dict = {}
205 display_dict['prompt'] = init_prompt
206 display_dict['status'] = init_status
Chun-Ta Linc482a5e2012-05-09 16:56:07 +0800207 display_dict['is_standard_status'] = is_standard_status
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800208
209 def prompt_label_expose(widget, event):
210 prompt = display_dict['prompt']
211 widget.set_text(prompt)
212
213 def status_label_expose(widget, event):
214 status = display_dict['status']
215 widget.set_text(status)
Chun-Ta Linc482a5e2012-05-09 16:56:07 +0800216 if is_standard_status:
217 widget.modify_fg(gtk.STATE_NORMAL, LABEL_COLORS[status])
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800218
219 prompt_label = make_label(
220 init_prompt, size=label_size,
221 alignment=(0, 0.5))
222 delimiter_label = make_label(':', alignment=(0, 0.5))
223 status_label = make_label(
224 init_status, size=label_size,
Chun-Ta Linc482a5e2012-05-09 16:56:07 +0800225 alignment=(0, 0.5))
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800226
227 widget = gtk.HBox()
228 widget.pack_end(status_label, False, False)
229 widget.pack_end(delimiter_label, False, False)
230 widget.pack_end(prompt_label, False, False)
231
232 status_label.connect('expose_event', status_label_expose)
233 prompt_label.connect('expose_event', prompt_label_expose)
234 return display_dict, widget
235
236
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800237def convert_pixels(size):
238 """Converts a pair in pixel that is suitable for current resolution.
239
240 GTK takes pixels as its unit in many function calls. To maintain the
241 consistency of the UI in different resolution, a conversion is required.
242 Take current resolution and (_UI_SCREEN_WIDTH, _UI_SCREEN_HEIGHT) as
243 the original resolution, this function returns a pair of width and height
244 that is converted for current resolution.
245
246 Because pixels in negative usually indicates unspecified, no conversion
247 will be done for negative pixels.
248
249 In addition, the aspect ratio is not maintained in this function.
250
251 Usage Example:
252 width,_ = convert_pixels((20,-1))
253
254 @param size: A pair of pixels that designed under original resolution.
255 @return: A pair of pixels of (width, height) format.
256 Pixels returned are always integer.
257 """
258 return (int(float(size[0]) / _UI_SCREEN_WIDTH * gtk.gdk.screen_width()
259 if (size[0] > 0) else size[0]),
260 int(float(size[1]) / _UI_SCREEN_HEIGHT * gtk.gdk.screen_height()
261 if (size[1] > 0) else size[1]))
262
263
264def make_hsep(height=1):
265 """Returns a widget acts as a horizontal separation line.
266
267 The unit is pixels under resolution _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
268 """
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800269 frame = gtk.EventBox()
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800270 # Convert height according to the current resolution.
271 frame.set_size_request(*convert_pixels((-1, height)))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800272 frame.modify_bg(gtk.STATE_NORMAL, SEP_COLOR)
273 return frame
274
275
276def make_vsep(width=1):
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800277 """Returns a widget acts as a vertical separation line.
278
279 The unit is pixels under resolution _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
280 """
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800281 frame = gtk.EventBox()
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800282 # Convert width according to the current resolution.
283 frame.set_size_request(*convert_pixels((width, -1)))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800284 frame.modify_bg(gtk.STATE_NORMAL, SEP_COLOR)
285 return frame
286
287
Hung-Te Linaf8ec542012-03-14 20:01:40 +0800288def make_countdown_widget(prompt=None, value=None, fg=LIGHT_GREEN):
289 if prompt is None:
290 prompt = 'time remaining / 剩餘時間: '
291 if value is None:
292 value = '%s' % FAIL_TIMEOUT
293 title = make_label(prompt, fg=fg, alignment=(1, 0.5))
294 countdown = make_label(value, fg=fg, alignment=(0, 0.5))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800295 hbox = gtk.HBox()
296 hbox.pack_start(title)
297 hbox.pack_start(countdown)
298 eb = gtk.EventBox()
299 eb.modify_bg(gtk.STATE_NORMAL, BLACK)
300 eb.add(hbox)
301 return eb, countdown
302
303
Jon Salzb1b39092012-05-03 02:05:09 +0800304def is_chrome_ui():
305 return os.environ.get('CROS_UI') == 'chrome'
306
307
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800308def hide_cursor(gdk_window):
309 pixmap = gtk.gdk.Pixmap(None, 1, 1, 1)
310 color = gtk.gdk.Color()
311 cursor = gtk.gdk.Cursor(pixmap, pixmap, color, color, 0, 0)
312 gdk_window.set_cursor(cursor)
313
314
315def calc_scale(wanted_x, wanted_y):
316 (widget_size_x, widget_size_y) = factory.get_shared_data('test_widget_size')
317 scale_x = (0.9 * widget_size_x) / wanted_x
318 scale_y = (0.9 * widget_size_y) / wanted_y
319 scale = scale_y if scale_y < scale_x else scale_x
320 scale = 1 if scale > 1 else scale
321 factory.log('scale: %s' % scale)
322 return scale
323
324
325def trim(text, length):
326 if len(text) > length:
327 text = text[:length-3] + '...'
328 return text
329
330
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800331class InputError(ValueError):
332 """Execption for input window callbacks to change status text message."""
333 pass
334
335
Hung-Te Linbf545582012-02-15 17:08:07 +0800336def make_input_window(prompt=None,
337 init_value=None,
338 msg_invalid=None,
339 font=None,
340 on_validate=None,
341 on_keypress=None,
342 on_complete=None):
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800343 """Creates a widget to prompt user for a valid string.
Hung-Te Linbf545582012-02-15 17:08:07 +0800344
345 @param prompt: A string to be displayed. None for default message.
346 @param init_value: Initial value to be set.
347 @param msg_invalid: Status string to display when input is invalid. None for
348 default message.
349 @param font: Font specification (string or pango.FontDescription) for label
350 and entry. None for default large font.
351 @param on_validate: A callback function to validate if the input from user
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800352 is valid. None for allowing any non-empty input. Any ValueError or
353 ui.InputError raised during execution in on_validate will be displayed
354 in bottom status.
Hung-Te Linbf545582012-02-15 17:08:07 +0800355 @param on_keypress: A callback function when each keystroke is hit.
356 @param on_complete: A callback function when a valid string is passed.
357 None to stop (gtk.main_quit).
Hung-Te Lin7130d9e2012-03-16 15:41:18 +0800358 @return: A widget with prompt, input entry, and status label. To access
359 these elements, use attribute 'prompt', 'entry', and 'label'.
Hung-Te Linbf545582012-02-15 17:08:07 +0800360 """
361 DEFAULT_MSG_INVALID = "Invalid input / 輸入不正確"
362 DEFAULT_PROMPT = "Enter Data / 輸入資料:"
363
364 def enter_callback(entry):
365 text = entry.get_text()
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800366 try:
367 if (on_validate and (not on_validate(text))) or (not text.strip()):
368 raise ValueError(msg_invalid)
Hung-Te Linbf545582012-02-15 17:08:07 +0800369 on_complete(text) if on_complete else gtk.main_quit()
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800370 except ValueError as e:
371 gtk.gdk.beep()
372 status_label.set_text('ERROR: %s' % e.message)
Hung-Te Linbf545582012-02-15 17:08:07 +0800373 return True
374
375 def key_press_callback(entry, key):
376 status_label.set_text('')
377 if on_keypress:
378 return on_keypress(entry, key)
379 return False
380
381 # Populate default parameters
382 if msg_invalid is None:
383 msg_invalid = DEFAULT_MSG_INVALID
384
385 if prompt is None:
386 prompt = DEFAULT_PROMPT
387
388 if font is None:
389 font = LABEL_LARGE_FONT
390 elif not isinstance(font, pango.FontDescription):
391 font = pango.FontDescription(font)
392
393 widget = gtk.VBox()
394 label = make_label(prompt, font=font)
395 status_label = make_label('', font=font)
396 entry = gtk.Entry()
397 entry.modify_font(font)
398 entry.connect("activate", enter_callback)
399 entry.connect("key_press_event", key_press_callback)
400 if init_value:
401 entry.set_text(init_value)
402 widget.modify_bg(gtk.STATE_NORMAL, BLACK)
403 status_label.modify_fg(gtk.STATE_NORMAL, RED)
404 widget.add(label)
405 widget.pack_start(entry)
406 widget.pack_start(status_label)
Chun-ta Lin2e7f44c2012-03-03 07:29:36 +0800407
Hung-Te Lin7130d9e2012-03-16 15:41:18 +0800408 widget.entry = entry
409 widget.status = status_label
410 widget.prompt = label
411
412 # TODO(itspeter) Replace deprecated get_entry by widget.entry.
Chun-ta Lin2e7f44c2012-03-03 07:29:36 +0800413 # Method for getting the entry.
414 widget.get_entry = lambda : entry
Hung-Te Linbf545582012-02-15 17:08:07 +0800415 return widget
416
417
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800418def make_summary_box(tests, state_map, rows=15):
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800419 '''Creates a widget display status of a set of test.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800420
421 @param tests: A list of FactoryTest nodes whose status (and children's
422 status) should be displayed.
423 @param state_map: The state map as provide by the state instance.
424 @param rows: The number of rows to display.
425 @return: A tuple (widget, label_map), where widget is the widget, and
426 label_map is a map from each test to the corresponding label.
427 '''
428 LABEL_EN_SIZE = (170, 35)
429 LABEL_EN_SIZE_2 = (450, 25)
430 LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
431
432 all_tests = sum([list(t.walk(in_order=True)) for t in tests], [])
433 columns = len(all_tests) / rows + (len(all_tests) % rows != 0)
434
435 info_box = gtk.HBox()
436 info_box.set_spacing(20)
437 for status in (TestState.ACTIVE, TestState.PASSED,
438 TestState.FAILED, TestState.UNTESTED):
439 label = make_label(status,
440 size=LABEL_EN_SIZE,
441 font=LABEL_EN_FONT,
442 alignment=(0.5, 0.5),
443 fg=LABEL_COLORS[status])
444 info_box.pack_start(label, False, False)
445
446 vbox = gtk.VBox()
447 vbox.set_spacing(20)
448 vbox.pack_start(info_box, False, False)
449
450 label_map = {}
451
452 if all_tests:
453 status_table = gtk.Table(rows, columns, True)
454 for (j, i), t in izip(product(xrange(columns), xrange(rows)),
455 all_tests):
456 msg_en = ' ' * (t.depth() - 1) + t.label_en
457 msg_en = trim(msg_en, 12)
458 if t.label_zh:
459 msg = '{0:<12} ({1})'.format(msg_en, t.label_zh)
460 else:
461 msg = msg_en
462 status = state_map[t].status
463 status_label = make_label(msg,
464 size=LABEL_EN_SIZE_2,
465 font=LABEL_EN_FONT,
466 alignment=(0.0, 0.5),
467 fg=LABEL_COLORS[status])
468 label_map[t] = status_label
469 status_table.attach(status_label, j, j+1, i, i+1)
470 vbox.pack_start(status_table, False, False)
471
472 return vbox, label_map
473
474
475def run_test_widget(dummy_job, test_widget,
476 invisible_cursor=True,
477 window_registration_callback=None,
478 cleanup_callback=None):
479 test_widget_size = factory.get_shared_data('test_widget_size')
480
481 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
482 window.modify_bg(gtk.STATE_NORMAL, BLACK)
483 window.set_size_request(*test_widget_size)
484
Jon Salzb1b39092012-05-03 02:05:09 +0800485 test_widget_position = factory.get_shared_data('test_widget_position')
486 if test_widget_position:
487 window.move(*test_widget_position)
488
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800489 def show_window():
490 window.show()
491 window.window.raise_() # pylint: disable=E1101
Jon Salzb1b39092012-05-03 02:05:09 +0800492 if is_chrome_ui():
493 window.present()
Jon Salzebb2fb72012-05-21 16:04:58 +0800494 window.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
Jon Salzb1b39092012-05-03 02:05:09 +0800495 else:
496 gtk.gdk.pointer_grab(window.window, confine_to=window.window)
Jon Salzebb2fb72012-05-21 16:04:58 +0800497 if invisible_cursor:
498 hide_cursor(window.window)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800499
500 test_path = factory.get_current_test_path()
501
502 def handle_event(event):
503 if (event.type == Event.Type.STATE_CHANGE and
504 test_path and event.path == test_path and
505 event.state.visible):
506 show_window()
507
508 event_client = EventClient(
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800509 callback=handle_event, event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800510
511 align = gtk.Alignment(xalign=0.5, yalign=0.5)
512 align.add(test_widget)
513
514 window.add(align)
515 for c in window.get_children():
516 # Show all children, but not the window itself yet.
517 c.show_all()
518
519 if window_registration_callback is not None:
520 window_registration_callback(window)
521
522 # Show the window if it is the visible test, or if the test_path is not
523 # available (e.g., run directly from the command line).
524 if (not test_path) or (
525 TestState.from_dict_or_object(
526 factory.get_state_instance().get_test_state(test_path)).visible):
527 show_window()
528 else:
529 window.hide()
530
Hung-Te Linde45e9c2012-03-19 13:02:06 +0800531 # When gtk.main() is running, it ignores all uncaught exceptions, which is
532 # not preferred by most of our factory tests. To prevent writing special
533 # function raising errors, we hook top level exception handler to always
534 # leave GTK main and raise exception again.
535
536 def exception_hook(exc_type, value, traceback):
537 # Prevent re-entrant.
538 sys.excepthook = old_excepthook
539 session['exception'] = (exc_type, value, traceback)
540 gobject.idle_add(gtk.main_quit)
541 return old_excepthook(exc_type, value, traceback)
542
543 session = {}
544 old_excepthook = sys.excepthook
545 sys.excepthook = exception_hook
546
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800547 gtk.main()
548
Jon Salzb1b39092012-05-03 02:05:09 +0800549 if not is_chrome_ui():
550 gtk.gdk.pointer_ungrab()
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800551
552 if cleanup_callback is not None:
553 cleanup_callback()
554
555 del event_client
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800556
Hung-Te Linde45e9c2012-03-19 13:02:06 +0800557 sys.excepthook = old_excepthook
558 exc_info = session.get('exception')
559 if exc_info is not None:
560 logging.error(exc_info[0], exc_info=exc_info)
Jon Salz6d0f8202012-07-02 14:02:25 +0800561 raise FactoryTestFailure(exc_info[1])
Hung-Te Linde45e9c2012-03-19 13:02:06 +0800562
563
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800564
565# ---------------------------------------------------------------------------
566# Server Implementation
567
568
569class Console(object):
570 '''Display a progress log. Implemented by launching an borderless
571 xterm at a strategic location, and running tail against the log.'''
572
573 def __init__(self, allocation):
Chun-Ta Lin734e96a2012-03-16 20:05:33 +0800574 # Specify how many lines and characters per line are displayed.
575 XTERM_DISPLAY_LINES = 13
576 XTERM_DISPLAY_CHARS = 120
577 # Extra space reserved for pixels between lines.
578 XTERM_RESERVED_LINES = 3
579
580 xterm_coords = '%dx%d+%d+%d' % (XTERM_DISPLAY_CHARS,
581 XTERM_DISPLAY_LINES,
582 allocation.x,
583 allocation.y)
584 xterm_reserved_height = gtk.gdk.screen_height() - allocation.y
585 font_size = int(float(xterm_reserved_height) / (XTERM_DISPLAY_LINES +
586 XTERM_RESERVED_LINES))
587 logging.info('xterm_reserved_height = %d' % xterm_reserved_height)
588 logging.info('font_size = %d' % font_size)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800589 logging.info('xterm_coords = %s', xterm_coords)
590 xterm_opts = ('-bg black -fg lightgray -bw 0 -g %s' % xterm_coords)
Chun-Ta Lin734e96a2012-03-16 20:05:33 +0800591 xterm_cmd = (
592 ['urxvt'] + xterm_opts.split() +
593 ['-fn', 'xft:DejaVu Sans Mono:pixelsize=%s' % font_size] +
594 ['-e', 'bash'] +
595 ['-c', 'tail -f "%s"' % factory.CONSOLE_LOG_PATH])
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800596 logging.info('xterm_cmd = %s', xterm_cmd)
597 self._proc = subprocess.Popen(xterm_cmd)
598
599 def __del__(self):
600 logging.info('console_proc __del__')
601 self._proc.kill()
602
603
604class TestLabelBox(gtk.EventBox): # pylint: disable=R0904
605
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800606 def __init__(self, test):
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800607 gtk.EventBox.__init__(self)
608 self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[TestState.UNTESTED])
Hung-Te Line94e0a02012-03-19 18:20:35 +0800609 self._is_group = test.is_group()
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800610 depth = len(test.get_ancestor_groups())
Hung-Te Line94e0a02012-03-19 18:20:35 +0800611 self._label_text = ' %s%s%s' % (
612 ' ' * depth,
613 SYMBOL_RIGHT_ARROW if self._is_group else ' ',
614 test.label_en)
615 if self._is_group:
616 self._label_text_collapsed = ' %s%s%s' % (
617 ' ' * depth,
618 SYMBOL_DOWN_ARROW if self._is_group else '',
619 test.label_en)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800620 self._label_en = make_label(
Hung-Te Line94e0a02012-03-19 18:20:35 +0800621 self._label_text, size=_LABEL_EN_SIZE,
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800622 font=_LABEL_EN_FONT, alignment=(0, 0.5),
623 fg=_LABEL_UNTESTED_FG)
624 self._label_zh = make_label(
625 test.label_zh, size=_LABEL_ZH_SIZE,
626 font=_LABEL_ZH_FONT, alignment=(0.5, 0.5),
627 fg=_LABEL_UNTESTED_FG)
628 self._label_t = make_label(
629 '', size=_LABEL_T_SIZE, font=_LABEL_T_FONT,
630 alignment=(0.5, 0.5), fg=BLACK)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800631 hbox = gtk.HBox()
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800632 hbox.pack_start(self._label_en, False, False)
633 hbox.pack_start(self._label_zh, False, False)
634 hbox.pack_start(self._label_t, False, False)
635 vbox = gtk.VBox()
636 vbox.pack_start(hbox, False, False)
637 vbox.pack_start(make_hsep(), False, False)
638 self.add(vbox)
639 self._status = None
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800640
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800641 def set_shortcut(self, shortcut):
642 if shortcut is None:
643 return
Hung-Te Lin99f64ef2012-03-22 11:05:45 +0800644 self._label_t.set_text('C-%s' % shortcut.upper())
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800645 attrs = self._label_en.get_attributes() or pango.AttrList()
646 attrs.filter(lambda attr: attr.type == pango.ATTR_UNDERLINE)
647 index_hotkey = self._label_en.get_text().upper().find(shortcut.upper())
648 if index_hotkey != -1:
649 attrs.insert(pango.AttrUnderline(
650 pango.UNDERLINE_LOW, index_hotkey, index_hotkey + 1))
651 attrs.insert(pango.AttrWeight(
652 pango.WEIGHT_BOLD, index_hotkey, index_hotkey + 1))
653 self._label_en.set_attributes(attrs)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800654 self.queue_draw()
655
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800656 def update(self, status):
657 if self._status == status:
658 return
659 self._status = status
660 label_fg = (_LABEL_UNTESTED_FG if status == TestState.UNTESTED
661 else BLACK)
Hung-Te Line94e0a02012-03-19 18:20:35 +0800662 if self._is_group:
663 self._label_en.set_text(
664 self._label_text_collapsed if status == TestState.ACTIVE
665 else self._label_text)
666
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800667 for label in [self._label_en, self._label_zh, self._label_t]:
668 label.modify_fg(gtk.STATE_NORMAL, label_fg)
669 self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[status])
670 self.queue_draw()
671
672
Hung-Te Lin96632362012-03-20 21:14:18 +0800673class ReviewInformation(object):
674
675 LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
676 TAB_BORDER = 20
677
678 def __init__(self, test_list):
679 self.test_list = test_list
680
681 def make_error_tab(self, test, state):
682 msg = '%s (%s)\n%s' % (test.label_en, test.label_zh,
683 str(state.error_msg))
684 label = make_label(msg, font=self.LABEL_EN_FONT, alignment=(0.0, 0.0))
685 label.set_line_wrap(True)
686 frame = gtk.Frame()
687 frame.add(label)
688 return frame
689
690 def make_widget(self):
691 bg_color = gtk.gdk.Color(0x1000, 0x1000, 0x1000)
692 self.notebook = gtk.Notebook()
693 self.notebook.modify_bg(gtk.STATE_NORMAL, bg_color)
694
695 test_list = self.test_list
696 state_map = test_list.get_state_map()
697 tab, _ = make_summary_box([test_list], state_map)
698 tab.set_border_width(self.TAB_BORDER)
699 self.notebook.append_page(tab, make_label('Summary'))
700
701 for i, t in izip(
702 count(1),
703 [t for t in test_list.walk()
704 if state_map[t].status == factory.TestState.FAILED
705 and t.is_leaf()]):
706 tab = self.make_error_tab(t, state_map[t])
707 tab.set_border_width(self.TAB_BORDER)
708 self.notebook.append_page(tab, make_label('#%02d' % i))
709
710 prompt = 'Review: Test Status Information'
711 if self.notebook.get_n_pages() > 1:
712 prompt += '\nPress left/right to change tabs'
713
714 control_label = make_label(prompt, font=self.LABEL_EN_FONT,
715 alignment=(0.5, 0.5))
716 vbox = gtk.VBox()
717 vbox.set_spacing(self.TAB_BORDER)
718 vbox.pack_start(control_label, False, False)
719 vbox.pack_start(self.notebook, False, False)
720 vbox.show_all()
721 vbox.grab_focus = self.notebook.grab_focus
722 return vbox
723
724
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800725class TestDirectory(gtk.VBox):
726 '''Widget containing a list of tests, colored by test status.
727
728 This is the widget corresponding to the RHS test panel.
729
730 Attributes:
731 _label_map: Dict of test path to TestLabelBox objects. Should
732 contain an entry for each test that has been visible at some
733 time.
734 _visible_status: List of (test, status) pairs reflecting the
735 last refresh of the set of visible tests. This is used to
736 rememeber what tests were active, to allow implementation of
737 visual refresh only when new active tests appear.
738 _shortcut_map: Dict of keyboard shortcut key to test path.
739 Tracks the current set of keyboard shortcut mappings for the
740 visible set of tests. This will change when the visible
741 test set changes.
742 '''
743
Tammo Spalinkddae4df2012-03-22 11:50:00 +0800744 def __init__(self, test_list):
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800745 gtk.VBox.__init__(self)
746 self.set_spacing(0)
747 self._label_map = {}
748 self._visible_status = []
749 self._shortcut_map = {}
Tammo Spalinkddae4df2012-03-22 11:50:00 +0800750 self._hard_shortcuts = set(
751 test.kbd_shortcut for test in test_list.walk()
752 if test.kbd_shortcut is not None)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800753
754 def _get_test_label(self, test):
755 if test.path in self._label_map:
756 return self._label_map[test.path]
757 label_box = TestLabelBox(test)
758 self._label_map[test.path] = label_box
759 return label_box
760
761 def _remove_shortcut(self, path):
762 reverse_map = dict((v, k) for k, v in self._shortcut_map.items())
763 if path not in reverse_map:
764 logging.error('Removal of non-present shortcut for %s' % path)
765 return
766 shortcut = reverse_map[path]
767 del self._shortcut_map[shortcut]
768
769 def _add_shortcut(self, test):
770 shortcut = test.kbd_shortcut
771 if shortcut in self._shortcut_map:
772 logging.error('Shortcut %s already in use by %s; cannot apply to %s'
773 % (shortcut, self._shortcut_map[shortcut], test.path))
774 shortcut = None
775 if shortcut is None:
776 # Find a suitable shortcut. For groups, use numbers. For
777 # regular tests, use alpha (letters).
Jon Salz0405ab52012-03-16 15:26:52 +0800778 if test.is_group():
779 gen = (x for x in string.digits if x not in self._shortcut_map)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800780 else:
Jon Salz0405ab52012-03-16 15:26:52 +0800781 gen = (x for x in test.label_en.lower() + string.lowercase
Tammo Spalinkddae4df2012-03-22 11:50:00 +0800782 if x.isalnum() and x not in self._shortcut_map
783 and x not in self._hard_shortcuts)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800784 shortcut = next(gen, None)
785 if shortcut is None:
786 logging.error('Unable to find shortcut for %s' % test.path)
787 return
788 self._shortcut_map[shortcut] = test.path
789 return shortcut
790
791 def handle_xevent(self, dummy_src, dummy_cond,
792 xhandle, keycode_map, event_client):
793 for dummy_i in range(0, xhandle.pending_events()):
794 xevent = xhandle.next_event()
795 if xevent.type != X.KeyPress:
796 continue
797 keycode = xevent.detail
798 if keycode not in keycode_map:
799 logging.warning('Ignoring unknown keycode %r' % keycode)
800 continue
801 shortcut = keycode_map[keycode]
Jon Salz0405ab52012-03-16 15:26:52 +0800802
Hung-Te Lin96632362012-03-20 21:14:18 +0800803 if (xevent.state & GLOBAL_HOT_KEY_MASK == GLOBAL_HOT_KEY_MASK):
Jon Salz0405ab52012-03-16 15:26:52 +0800804 event_type = GLOBAL_HOT_KEY_EVENTS.get(shortcut)
805 if event_type:
806 event_client.post_event(Event(event_type))
807 else:
Jon Salz968e90b2012-03-18 16:12:43 +0800808 logging.warning('Unbound global hot key %s', key)
Jon Salz0405ab52012-03-16 15:26:52 +0800809 else:
810 if shortcut not in self._shortcut_map:
811 logging.warning('Ignoring unbound shortcut %r' % shortcut)
812 continue
813 test_path = self._shortcut_map[shortcut]
814 event_client.post_event(Event(Event.Type.SWITCH_TEST,
815 path=test_path))
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800816 return True
817
818 def update(self, new_test_status):
819 '''Refresh the RHS test list to show current status and active groups.
820
821 Refresh the set of visible tests only when new active tests
822 arise. This avoids visual volatility when switching between
823 tests (intervals where no test is active). Also refresh at
824 initial startup.
825
826 Args:
827 new_test_status: A list of (test, status) tuples. The tests
828 order should match how they should be displayed in the
829 directory (rhs panel).
830 '''
831 old_active = set(t for t, s in self._visible_status
832 if s == TestState.ACTIVE)
833 new_active = set(t for t, s in new_test_status
834 if s == TestState.ACTIVE)
835 new_visible = set(t for t, s in new_test_status)
836 old_visible = set(t for t, s in self._visible_status)
837
838 if old_active and not new_active - old_active:
839 # No new active tests, so do not change the displayed test
840 # set, only update the displayed status for currently
841 # visible tests. Not updating _visible_status allows us
842 # to remember the last set of active tests.
843 for test, _ in self._visible_status:
844 status = test.get_state().status
845 self._label_map[test.path].update(status)
846 return
847
848 self._visible_status = new_test_status
849
850 new_test_map = dict((t.path, t) for t, s in new_test_status)
851
852 for test in old_visible - new_visible:
853 label_box = self._label_map[test.path]
854 logging.debug('removing %s test label' % test.path)
855 self.remove(label_box)
856 self._remove_shortcut(test.path)
857
858 new_tests = new_visible - old_visible
859
860 for position, (test, status) in enumerate(new_test_status):
861 label_box = self._get_test_label(test)
862 if test in new_tests:
863 shortcut = self._add_shortcut(test)
864 label_box = self._get_test_label(test)
865 label_box.set_shortcut(shortcut)
866 logging.debug('adding %s test label (sortcut %r, pos %d)' %
867 (test.path, shortcut, position))
868 self.pack_start(label_box, False, False)
869 self.reorder_child(label_box, position)
870 label_box.update(status)
871
872 self.show_all()
873
874
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800875
876class UiState(object):
877
Hung-Te Lin96632362012-03-20 21:14:18 +0800878 WIDGET_NONE = 0
879 WIDGET_IDLE = 1
880 WIDGET_SUMMARY = 2
881 WIDGET_REVIEW = 3
882
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800883 def __init__(self, test_widget_box, test_directory_widget, test_list):
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800884 self._test_widget_box = test_widget_box
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800885 self._test_directory_widget = test_directory_widget
886 self._test_list = test_list
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800887 self._transition_count = 0
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800888 self._active_test_label_map = None
Hung-Te Lin96632362012-03-20 21:14:18 +0800889 self._active_widget = self.WIDGET_NONE
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800890 self.update_test_state()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800891
Hung-Te Lin96632362012-03-20 21:14:18 +0800892 def show_idle_widget(self):
893 self.remove_state_widget()
894 self._test_widget_box.set(0.5, 0.5, 0.0, 0.0)
895 self._test_widget_box.set_padding(0, 0, 0, 0)
896 label = make_label(MESSAGE_NO_ACTIVE_TESTS,
897 font=_OTHER_LABEL_FONT,
898 alignment=(0.5, 0.5))
899 self._test_widget_box.add(label)
900 self._test_widget_box.show_all()
901 self._active_widget = self.WIDGET_IDLE
902
903 def show_summary_widget(self):
904 self.remove_state_widget()
905 state_map = self._test_list.get_state_map()
906 self._test_widget_box.set(0.5, 0.0, 0.0, 0.0)
907 self._test_widget_box.set_padding(40, 0, 0, 0)
908 vbox, self._active_test_label_map = make_summary_box(
909 [t for t in self._test_list.subtests
910 if state_map[t].status == TestState.ACTIVE],
911 state_map)
912 self._test_widget_box.add(vbox)
913 self._test_widget_box.show_all()
914 self._active_widget = self.WIDGET_SUMMARY
915
916 def show_review_widget(self):
917 self.remove_state_widget()
918 self._review_request = False
919 self._test_widget_box.set(0.5, 0.5, 0.0, 0.0)
920 self._test_widget_box.set_padding(0, 0, 0, 0)
921 widget = ReviewInformation(self._test_list).make_widget()
922 self._test_widget_box.add(widget)
923 self._test_widget_box.show_all()
924 widget.grab_focus()
925 self._active_widget = self.WIDGET_REVIEW
926
927 def remove_state_widget(self):
928 for child in self._test_widget_box.get_children():
929 child.hide()
930 self._test_widget_box.remove(child)
931 self._active_test_label_map = None
932 self._active_widget = self.WIDGET_NONE
933
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800934 def update_test_state(self):
935 state_map = self._test_list.get_state_map()
936 active_tests = set(
937 t for t in self._test_list.walk()
938 if t.is_leaf() and state_map[t].status == TestState.ACTIVE)
939 active_groups = set(g for t in active_tests
940 for g in t.get_ancestor_groups())
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800941
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800942 def filter_visible_test_state(tests):
943 '''List currently visible tests and their status.
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800944
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800945 Visible means currently displayed in the RHS panel.
946 Visiblity is implied by being a top level test or having
947 membership in a group with at least one active test.
948
949 Returns:
950 A list of (test, status) tuples for all visible tests,
951 in the order they should be displayed.
952 '''
953 results = []
954 for test in tests:
Jon Salz0405ab52012-03-16 15:26:52 +0800955 if test.is_group():
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800956 results.append((test, TestState.UNTESTED))
957 if test not in active_groups:
958 continue
959 results += filter_visible_test_state(test.subtests)
960 else:
961 results.append((test, state_map[test].status))
962 return results
963
964 visible_test_state = filter_visible_test_state(self._test_list.subtests)
965 self._test_directory_widget.update(visible_test_state)
966
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800967 if not active_tests:
Hung-Te Lin96632362012-03-20 21:14:18 +0800968 # Display the idle or review information screen.
969 def waiting_for_transition():
970 return (self._active_widget not in
971 [self.WIDGET_REVIEW, self.WIDGET_IDLE])
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800972
Hung-Te Lin96632362012-03-20 21:14:18 +0800973 # For smooth transition between tests, idle widget if activated only
974 # after _NO_ACTIVE_TEST_DELAY_MS without state change.
975 def idle_transition_check(cookie):
976 if (waiting_for_transition() and
977 cookie == self._transition_count):
978 self._transition_count += 1
979 self.show_idle_widget()
980 return False
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800981
Hung-Te Lin96632362012-03-20 21:14:18 +0800982 if waiting_for_transition():
983 gobject.timeout_add(_NO_ACTIVE_TEST_DELAY_MS,
984 idle_transition_check,
985 self._transition_count)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800986 return
987
988 self._transition_count += 1
989
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800990 if any(t.has_ui for t in active_tests):
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800991 # Remove the widget (if any) since there is an active test
992 # with a UI.
Hung-Te Lin96632362012-03-20 21:14:18 +0800993 self.remove_state_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800994 return
995
996 if (self._active_test_label_map is not None and
997 all(t in self._active_test_label_map for t in active_tests)):
998 # All active tests are already present in the summary, so just
999 # update their states.
1000 for test, label in self._active_test_label_map.iteritems():
1001 label.modify_fg(
1002 gtk.STATE_NORMAL,
1003 LABEL_COLORS[state_map[test].status])
1004 return
1005
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001006 # No active UI; draw summary of current test states
Hung-Te Lin96632362012-03-20 21:14:18 +08001007 self.show_summary_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001008
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001009
1010def grab_shortcut_keys(disp, event_handler, event_client):
1011 # We want to receive KeyPress events
1012 root = disp.screen().root
1013 root.change_attributes(event_mask = X.KeyPressMask)
1014 shortcut_set = set(string.lowercase + string.digits)
1015 keycode_map = {}
1016 for mod, shortcut in ([(X.ControlMask, k) for k in shortcut_set] +
Jon Salz0405ab52012-03-16 15:26:52 +08001017 [(GLOBAL_HOT_KEY_MASK, k)
1018 for k in GLOBAL_HOT_KEY_EVENTS] +
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001019 [(X.Mod1Mask, 'Tab')]): # Mod1 = Alt
1020 keysym = gtk.gdk.keyval_from_name(shortcut)
1021 keycode = disp.keysym_to_keycode(keysym)
1022 keycode_map[keycode] = shortcut
1023 root.grab_key(keycode, mod, 1, X.GrabModeAsync, X.GrabModeAsync)
1024 # This flushes the XGrabKey calls to the server.
1025 for dummy_x in range(0, root.display.pending_events()):
1026 root.display.next_event()
1027 gobject.io_add_watch(root.display, gobject.IO_IN, event_handler,
1028 root.display, keycode_map, event_client)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001029
1030
Jon Salz5da61e62012-05-31 13:06:22 +08001031def start_reposition_thread(title_regexp):
1032 '''Starts a thread to reposition a client window once it appears.
1033
1034 This is useful to avoid blocking the console.
1035
1036 Args:
1037 title_regexp: A regexp for the window's title (used to find the
1038 window to reposition).
1039 '''
1040 test_widget_position = (
1041 factory.get_shared_data('test_widget_position'))
1042 if not test_widget_position:
1043 return
1044
1045 def reposition():
1046 display = Display()
1047 root = display.screen().root
1048 for i in xrange(50):
1049 wins = [win for win in root.query_tree().children
1050 if re.match(title_regexp, win.get_wm_name())]
1051 if wins:
1052 wins[0].configure(x=test_widget_position[0],
1053 y=test_widget_position[1])
1054 display.sync()
1055 return
1056 # Wait 100 ms and try again.
1057 time.sleep(.1)
1058 thread = threading.Thread(target=reposition)
1059 thread.daemon = True
1060 thread.start()
1061
1062
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001063def main(test_list_path):
1064 '''Starts the main UI.
1065
1066 This is launched by the autotest/cros/factory/client.
1067 When operators press keyboard shortcuts, the shortcut
1068 value is sent as an event to the control program.'''
1069
1070 test_list = None
1071 ui_state = None
1072 event_client = None
1073
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001074 def handle_key_release_event(_, event):
1075 logging.info('base ui key event (%s)', event.keyval)
1076 return True
1077
1078 def handle_event(event):
1079 if event.type == Event.Type.STATE_CHANGE:
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001080 ui_state.update_test_state()
Hung-Te Lin96632362012-03-20 21:14:18 +08001081 elif event.type == Event.Type.REVIEW:
1082 logging.info("Operator activates review information screen")
1083 ui_state.show_review_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001084
1085 test_list = factory.read_test_list(test_list_path)
1086
1087 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
1088 window.connect('destroy', lambda _: gtk.main_quit())
1089 window.modify_bg(gtk.STATE_NORMAL, BLACK)
1090
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001091 disp = Display()
1092
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001093 event_client = EventClient(
1094 callback=handle_event,
1095 event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
1096
1097 screen = window.get_screen()
1098 if (screen is None):
1099 logging.info('ERROR: communication with the X server is not working, ' +
1100 'could not find a working screen. UI exiting.')
1101 sys.exit(1)
1102
1103 screen_size_str = os.environ.get('CROS_SCREEN_SIZE')
1104 if screen_size_str:
1105 match = re.match(r'^(\d+)x(\d+)$', screen_size_str)
1106 assert match, 'CROS_SCREEN_SIZE should be {width}x{height}'
1107 screen_size = (int(match.group(1)), int(match.group(2)))
1108 else:
1109 screen_size = (screen.get_width(), screen.get_height())
1110 window.set_size_request(*screen_size)
1111
Tammo Spalinkddae4df2012-03-22 11:50:00 +08001112 test_directory = TestDirectory(test_list)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001113
1114 rhs_box = gtk.EventBox()
1115 rhs_box.modify_bg(gtk.STATE_NORMAL, _LABEL_TROUGH_COLOR)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001116 rhs_box.add(test_directory)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001117
1118 console_box = gtk.EventBox()
Chun-Ta Lin734e96a2012-03-16 20:05:33 +08001119 console_box.set_size_request(*convert_pixels((-1, 180)))
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001120 console_box.modify_bg(gtk.STATE_NORMAL, BLACK)
1121
1122 test_widget_box = gtk.Alignment()
1123 test_widget_box.set_size_request(-1, -1)
1124
1125 lhs_box = gtk.VBox()
1126 lhs_box.pack_end(console_box, False, False)
1127 lhs_box.pack_start(test_widget_box)
1128 lhs_box.pack_start(make_hsep(3), False, False)
1129
1130 base_box = gtk.HBox()
1131 base_box.pack_end(rhs_box, False, False)
1132 base_box.pack_end(make_vsep(3), False, False)
1133 base_box.pack_start(lhs_box)
1134
1135 window.connect('key-release-event', handle_key_release_event)
1136 window.add_events(gtk.gdk.KEY_RELEASE_MASK)
1137
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001138 ui_state = UiState(test_widget_box, test_directory, test_list)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001139
1140 window.add(base_box)
1141 window.show_all()
1142
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001143 grab_shortcut_keys(disp, test_directory.handle_xevent, event_client)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001144
1145 hide_cursor(window.window)
1146
1147 test_widget_allocation = test_widget_box.get_allocation()
1148 test_widget_size = (test_widget_allocation.width,
1149 test_widget_allocation.height)
1150 factory.set_shared_data('test_widget_size', test_widget_size)
1151
Jon Salz758e6cc2012-04-03 15:47:07 +08001152 if not factory.in_chroot():
1153 dummy_console = Console(console_box.get_allocation())
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001154
1155 event_client.post_event(Event(Event.Type.UI_READY))
1156
1157 logging.info('cros/factory/ui setup done, starting gtk.main()...')
1158 gtk.main()
1159 logging.info('cros/factory/ui gtk.main() finished, exiting.')
1160
1161
1162if __name__ == '__main__':
Jon Salz14bcbb02012-03-17 15:11:50 +08001163 parser = OptionParser(usage='usage: %prog [options] TEST-LIST-PATH')
1164 parser.add_option('-v', '--verbose', dest='verbose',
1165 action='store_true',
1166 help='Enable debug logging')
1167 (options, args) = parser.parse_args()
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001168
Jon Salz14bcbb02012-03-17 15:11:50 +08001169 if len(args) != 1:
1170 parser.error('Incorrect number of arguments')
1171
1172 factory.init_logging('ui', verbose=options.verbose)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001173 main(sys.argv[1])