blob: a3c4ef75cf8f142b7eb7e6436b75f47b101274ee [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
Jon Salz0697cbf2012-07-04 15:14:04 +080022# 'test list' panel on the right hand side of the screen. The
Hung-Te Lin6bb48552012-02-09 14:37:43 +080023# majority of the screen is dedicated to tests, which are executed in
24# seperate processes, but instructed to display their own UIs in this
Jon Salz0697cbf2012-07-04 15:14:04 +080025# dedicated area whenever possible. Tests in the test list are
Hung-Te Lin6bb48552012-02-09 14:37:43 +080026# executed in order by default, but can be activated on demand via
Jon Salz0697cbf2012-07-04 15:14:04 +080027# associated keyboard shortcuts. As tests are run, their status is
Hung-Te Lin6bb48552012-02-09 14:37:43 +080028# 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
Jon Salz0697cbf2012-07-04 15:14:04 +080048# image build process host-depends list. Failure to load in
Tammo Spalinkdb8d7112012-03-13 18:54:37 +080049# production should always manifest during regular use.
50try:
Jon Salz0697cbf2012-07-04 15:14:04 +080051 from Xlib import X
52 from Xlib.display import Display
Tammo Spalinkdb8d7112012-03-13 18:54:37 +080053except:
Jon Salz0697cbf2012-07-04 15:14:04 +080054 pass
Tammo Spalinkdb8d7112012-03-13 18:54:37 +080055
Hung-Te Lin6bb48552012-02-09 14:37:43 +080056# Factory and autotest modules
Jon Salz0697cbf2012-07-04 15:14:04 +080057import factory_common # pylint: disable=W0611
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()
Jon Salz0697cbf2012-07-04 15:14:04 +080076RED = gtk.gdk.Color(0xFFFF, 0, 0)
Hung-Te Linf2f78f72012-02-08 19:27:11 +080077GREEN = gtk.gdk.Color(0, 0xFFFF, 0)
Jon Salz0697cbf2012-07-04 15:14:04 +080078BLUE = gtk.gdk.Color(0, 0, 0xFFFF)
Hung-Te Linf2f78f72012-02-08 19:27:11 +080079WHITE = 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 = {
Jon Salz0697cbf2012-07-04 15:14:04 +080088 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')}
Hung-Te Linf2f78f72012-02-08 19:27:11 +080092
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 = (
Jon Salz0697cbf2012-07-04 15:14:04 +080099 "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"
104 " Re-test failed items.\n\n"
105 "Ctrl-Alt-R (Reset):\n"
106 " Re-test everything.\n\n"
107 "Ctrl-Alt-Z (Information):\n"
108 " Review test results and information.\n\n"
109 )
Hung-Te Line94e0a02012-03-19 18:20:35 +0800110
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800111USER_PASS_FAIL_SELECT_STR = (
Jon Salz0697cbf2012-07-04 15:14:04 +0800112 '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(
Jon Salz0697cbf2012-07-04 15:14:04 +0800129 'courier new bold extra-condensed 16')
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800130_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 = {
Jon Salz0697cbf2012-07-04 15:14:04 +0800135 'r': Event.Type.RESTART_TESTS,
136 'a': Event.Type.AUTO_RUN,
137 'f': Event.Type.RE_RUN_FAILED,
138 'z': Event.Type.REVIEW,
139 }
Jon Salz0405ab52012-03-16 15:26:52 +0800140try:
Jon Salz0697cbf2012-07-04 15:14:04 +0800141 # Works only if X is available.
142 GLOBAL_HOT_KEY_MASK = X.ControlMask | X.Mod1Mask
Jon Salz0405ab52012-03-16 15:26:52 +0800143except:
Jon Salz0697cbf2012-07-04 15:14:04 +0800144 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):
Jon Salz0697cbf2012-07-04 15:14:04 +0800153 __enter__ = gtk.gdk.threads_enter
154 def __exit__(*ignored):
155 gtk.gdk.threads_leave()
Hung-Te Lin4b0b61f2012-03-05 14:20:06 +0800156
157
158gtk_lock = _GtkLock()
159
160
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800161def make_label(message, font=LABEL_FONT, fg=LIGHT_GREEN,
Jon Salz0697cbf2012-07-04 15:14:04 +0800162 size=None, alignment=None):
163 """Returns a label widget.
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800164
Jon Salz0697cbf2012-07-04 15:14:04 +0800165 A wrapper for gtk.Label. The unit of size is pixels under resolution
166 _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800167
Jon Salz0697cbf2012-07-04 15:14:04 +0800168 @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 """
175 l = gtk.Label(message)
176 l.modify_font(font)
177 l.modify_fg(gtk.STATE_NORMAL, fg)
178 if size:
179 # Convert size according to the current resolution.
180 l.set_size_request(*convert_pixels(size))
181 if alignment:
182 l.set_alignment(*alignment)
183 return l
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800184
185
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800186def make_status_row(init_prompt,
Jon Salz0697cbf2012-07-04 15:14:04 +0800187 init_status,
188 label_size=_LABEL_STATUS_ROW_SIZE,
189 is_standard_status=True):
190 """Returns a widget that live updates prompt and status in a row.
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800191
Jon Salz0697cbf2012-07-04 15:14:04 +0800192 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.
196 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
Jon Salz0697cbf2012-07-04 15:14:04 +0800200 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
207 display_dict['is_standard_status'] = is_standard_status
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800208
Jon Salz0697cbf2012-07-04 15:14:04 +0800209 def prompt_label_expose(widget, event):
210 prompt = display_dict['prompt']
211 widget.set_text(prompt)
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800212
Jon Salz0697cbf2012-07-04 15:14:04 +0800213 def status_label_expose(widget, event):
214 status = display_dict['status']
215 widget.set_text(status)
216 if is_standard_status:
217 widget.modify_fg(gtk.STATE_NORMAL, LABEL_COLORS[status])
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800218
Jon Salz0697cbf2012-07-04 15:14:04 +0800219 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,
225 alignment=(0, 0.5))
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800226
Jon Salz0697cbf2012-07-04 15:14:04 +0800227 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)
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800231
Jon Salz0697cbf2012-07-04 15:14:04 +0800232 status_label.connect('expose_event', status_label_expose)
233 prompt_label.connect('expose_event', prompt_label_expose)
234 return display_dict, widget
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800235
236
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800237def convert_pixels(size):
Jon Salz0697cbf2012-07-04 15:14:04 +0800238 """Converts a pair in pixel that is suitable for current resolution.
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800239
Jon Salz0697cbf2012-07-04 15:14:04 +0800240 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.
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800245
Jon Salz0697cbf2012-07-04 15:14:04 +0800246 Because pixels in negative usually indicates unspecified, no conversion
247 will be done for negative pixels.
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800248
Jon Salz0697cbf2012-07-04 15:14:04 +0800249 In addition, the aspect ratio is not maintained in this function.
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800250
Jon Salz0697cbf2012-07-04 15:14:04 +0800251 Usage Example:
252 width,_ = convert_pixels((20,-1))
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800253
Jon Salz0697cbf2012-07-04 15:14:04 +0800254 @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]))
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800262
263
264def make_hsep(height=1):
Jon Salz0697cbf2012-07-04 15:14:04 +0800265 """Returns a widget acts as a horizontal separation line.
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800266
Jon Salz0697cbf2012-07-04 15:14:04 +0800267 The unit is pixels under resolution _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
268 """
269 frame = gtk.EventBox()
270 # Convert height according to the current resolution.
271 frame.set_size_request(*convert_pixels((-1, height)))
272 frame.modify_bg(gtk.STATE_NORMAL, SEP_COLOR)
273 return frame
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800274
275
276def make_vsep(width=1):
Jon Salz0697cbf2012-07-04 15:14:04 +0800277 """Returns a widget acts as a vertical separation line.
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800278
Jon Salz0697cbf2012-07-04 15:14:04 +0800279 The unit is pixels under resolution _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
280 """
281 frame = gtk.EventBox()
282 # Convert width according to the current resolution.
283 frame.set_size_request(*convert_pixels((width, -1)))
284 frame.modify_bg(gtk.STATE_NORMAL, SEP_COLOR)
285 return frame
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800286
287
Hung-Te Linaf8ec542012-03-14 20:01:40 +0800288def make_countdown_widget(prompt=None, value=None, fg=LIGHT_GREEN):
Jon Salz0697cbf2012-07-04 15:14:04 +0800289 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))
295 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
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800302
303
Jon Salzb1b39092012-05-03 02:05:09 +0800304def is_chrome_ui():
Jon Salz0697cbf2012-07-04 15:14:04 +0800305 return os.environ.get('CROS_UI') == 'chrome'
Jon Salzb1b39092012-05-03 02:05:09 +0800306
307
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800308def hide_cursor(gdk_window):
Jon Salz0697cbf2012-07-04 15:14:04 +0800309 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)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800313
314
315def calc_scale(wanted_x, wanted_y):
Jon Salz0697cbf2012-07-04 15:14:04 +0800316 (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
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800323
324
325def trim(text, length):
Jon Salz0697cbf2012-07-04 15:14:04 +0800326 if len(text) > length:
327 text = text[:length-3] + '...'
328 return text
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800329
330
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800331class InputError(ValueError):
Jon Salz0697cbf2012-07-04 15:14:04 +0800332 """Execption for input window callbacks to change status text message."""
333 pass
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800334
335
Hung-Te Linbf545582012-02-15 17:08:07 +0800336def make_input_window(prompt=None,
Jon Salz0697cbf2012-07-04 15:14:04 +0800337 init_value=None,
338 msg_invalid=None,
339 font=None,
340 on_validate=None,
341 on_keypress=None,
342 on_complete=None):
343 """Creates a widget to prompt user for a valid string.
Hung-Te Linbf545582012-02-15 17:08:07 +0800344
Jon Salz0697cbf2012-07-04 15:14:04 +0800345 @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
352 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.
355 @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).
358 @return: A widget with prompt, input entry, and status label. To access
359 these elements, use attribute 'prompt', 'entry', and 'label'.
360 """
361 DEFAULT_MSG_INVALID = "Invalid input / 輸入不正確"
362 DEFAULT_PROMPT = "Enter Data / 輸入資料:"
Hung-Te Linbf545582012-02-15 17:08:07 +0800363
Jon Salz0697cbf2012-07-04 15:14:04 +0800364 def enter_callback(entry):
365 text = entry.get_text()
366 try:
367 if (on_validate and (not on_validate(text))) or (not text.strip()):
368 raise ValueError(msg_invalid)
369 on_complete(text) if on_complete else gtk.main_quit()
370 except ValueError as e:
371 gtk.gdk.beep()
372 status_label.set_text('ERROR: %s' % e.message)
373 return True
Hung-Te Linbf545582012-02-15 17:08:07 +0800374
Jon Salz0697cbf2012-07-04 15:14:04 +0800375 def key_press_callback(entry, key):
376 status_label.set_text('')
377 if on_keypress:
378 return on_keypress(entry, key)
379 return False
Hung-Te Linbf545582012-02-15 17:08:07 +0800380
Jon Salz0697cbf2012-07-04 15:14:04 +0800381 # Populate default parameters
382 if msg_invalid is None:
383 msg_invalid = DEFAULT_MSG_INVALID
Hung-Te Linbf545582012-02-15 17:08:07 +0800384
Jon Salz0697cbf2012-07-04 15:14:04 +0800385 if prompt is None:
386 prompt = DEFAULT_PROMPT
Hung-Te Linbf545582012-02-15 17:08:07 +0800387
Jon Salz0697cbf2012-07-04 15:14:04 +0800388 if font is None:
389 font = LABEL_LARGE_FONT
390 elif not isinstance(font, pango.FontDescription):
391 font = pango.FontDescription(font)
Hung-Te Linbf545582012-02-15 17:08:07 +0800392
Jon Salz0697cbf2012-07-04 15:14:04 +0800393 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
Jon Salz0697cbf2012-07-04 15:14:04 +0800408 widget.entry = entry
409 widget.status = status_label
410 widget.prompt = label
Hung-Te Lin7130d9e2012-03-16 15:41:18 +0800411
Jon Salz0697cbf2012-07-04 15:14:04 +0800412 # TODO(itspeter) Replace deprecated get_entry by widget.entry.
413 # Method for getting the entry.
414 widget.get_entry = lambda : entry
415 return widget
Hung-Te Linbf545582012-02-15 17:08:07 +0800416
417
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800418def make_summary_box(tests, state_map, rows=15):
Jon Salz0697cbf2012-07-04 15:14:04 +0800419 '''Creates a widget display status of a set of test.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800420
Jon Salz0697cbf2012-07-04 15:14:04 +0800421 @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')
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800431
Jon Salz0697cbf2012-07-04 15:14:04 +0800432 all_tests = sum([list(t.walk(in_order=True)) for t in tests], [])
433 columns = len(all_tests) / rows + (len(all_tests) % rows != 0)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800434
Jon Salz0697cbf2012-07-04 15:14:04 +0800435 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)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800445
Jon Salz0697cbf2012-07-04 15:14:04 +0800446 vbox = gtk.VBox()
447 vbox.set_spacing(20)
448 vbox.pack_start(info_box, False, False)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800449
Jon Salz0697cbf2012-07-04 15:14:04 +0800450 label_map = {}
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800451
Jon Salz0697cbf2012-07-04 15:14:04 +0800452 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)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800471
Jon Salz0697cbf2012-07-04 15:14:04 +0800472 return vbox, label_map
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800473
474
475def run_test_widget(dummy_job, test_widget,
Jon Salz0697cbf2012-07-04 15:14:04 +0800476 invisible_cursor=True,
477 window_registration_callback=None,
478 cleanup_callback=None):
479 test_widget_size = factory.get_shared_data('test_widget_size')
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800480
Jon Salz0697cbf2012-07-04 15:14:04 +0800481 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
482 window.modify_bg(gtk.STATE_NORMAL, BLACK)
483 window.set_size_request(*test_widget_size)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800484
Jon Salz0697cbf2012-07-04 15:14:04 +0800485 test_widget_position = factory.get_shared_data('test_widget_position')
486 if test_widget_position:
487 window.move(*test_widget_position)
Jon Salzb1b39092012-05-03 02:05:09 +0800488
Jon Salz0697cbf2012-07-04 15:14:04 +0800489 def show_window():
490 window.show()
491 window.window.raise_() # pylint: disable=E1101
492 if is_chrome_ui():
493 window.present()
494 window.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800495 else:
Jon Salz0697cbf2012-07-04 15:14:04 +0800496 gtk.gdk.pointer_grab(window.window, confine_to=window.window)
497 if invisible_cursor:
498 hide_cursor(window.window)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800499
Jon Salz0697cbf2012-07-04 15:14:04 +0800500 test_path = factory.get_current_test_path()
Hung-Te Linde45e9c2012-03-19 13:02:06 +0800501
Jon Salz0697cbf2012-07-04 15:14:04 +0800502 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()
Hung-Te Linde45e9c2012-03-19 13:02:06 +0800507
Jon Salz0697cbf2012-07-04 15:14:04 +0800508 event_client = EventClient(
509 callback=handle_event, event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
Hung-Te Linde45e9c2012-03-19 13:02:06 +0800510
Jon Salz0697cbf2012-07-04 15:14:04 +0800511 align = gtk.Alignment(xalign=0.5, yalign=0.5)
512 align.add(test_widget)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800513
Jon Salz0697cbf2012-07-04 15:14:04 +0800514 window.add(align)
515 for c in window.get_children():
516 # Show all children, but not the window itself yet.
517 c.show_all()
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800518
Jon Salz0697cbf2012-07-04 15:14:04 +0800519 if window_registration_callback is not None:
520 window_registration_callback(window)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800521
Jon Salz0697cbf2012-07-04 15:14:04 +0800522 # 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()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800530
Jon Salz0697cbf2012-07-04 15:14:04 +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.
Hung-Te Linde45e9c2012-03-19 13:02:06 +0800538 sys.excepthook = old_excepthook
Jon Salz0697cbf2012-07-04 15:14:04 +0800539 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
547 gtk.main()
548
549 if not is_chrome_ui():
550 gtk.gdk.pointer_ungrab()
551
552 if cleanup_callback is not None:
553 cleanup_callback()
554
555 del event_client
556
557 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)
561 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):
Jon Salz0697cbf2012-07-04 15:14:04 +0800570 '''Display a progress log. Implemented by launching an borderless
571 xterm at a strategic location, and running tail against the log.'''
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800572
Jon Salz0697cbf2012-07-04 15:14:04 +0800573 def __init__(self, allocation):
574 # 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
Chun-Ta Lin734e96a2012-03-16 20:05:33 +0800579
Jon Salz0697cbf2012-07-04 15:14:04 +0800580 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)
589 logging.info('xterm_coords = %s', xterm_coords)
590 xterm_opts = ('-bg black -fg lightgray -bw 0 -g %s' % xterm_coords)
591 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])
596 logging.info('xterm_cmd = %s', xterm_cmd)
597 self._proc = subprocess.Popen(xterm_cmd)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800598
Jon Salz0697cbf2012-07-04 15:14:04 +0800599 def __del__(self):
600 logging.info('console_proc __del__')
601 self._proc.kill()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800602
603
Jon Salz0697cbf2012-07-04 15:14:04 +0800604class TestLabelBox(gtk.EventBox): # pylint: disable=R0904
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800605
Jon Salz0697cbf2012-07-04 15:14:04 +0800606 def __init__(self, test):
607 gtk.EventBox.__init__(self)
608 self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[TestState.UNTESTED])
609 self._is_group = test.is_group()
610 depth = len(test.get_ancestor_groups())
611 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)
620 self._label_en = make_label(
621 self._label_text, size=_LABEL_EN_SIZE,
622 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)
631 hbox = gtk.HBox()
632 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
Jon Salz0697cbf2012-07-04 15:14:04 +0800641 def set_shortcut(self, shortcut):
642 if shortcut is None:
643 return
644 self._label_t.set_text('C-%s' % shortcut.upper())
645 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)
654 self.queue_draw()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800655
Jon Salz0697cbf2012-07-04 15:14:04 +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)
662 if self._is_group:
663 self._label_en.set_text(
664 self._label_text_collapsed if status == TestState.ACTIVE
665 else self._label_text)
Hung-Te Line94e0a02012-03-19 18:20:35 +0800666
Jon Salz0697cbf2012-07-04 15:14:04 +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()
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800671
672
Hung-Te Lin96632362012-03-20 21:14:18 +0800673class ReviewInformation(object):
674
Jon Salz0697cbf2012-07-04 15:14:04 +0800675 LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
676 TAB_BORDER = 20
Hung-Te Lin96632362012-03-20 21:14:18 +0800677
Jon Salz0697cbf2012-07-04 15:14:04 +0800678 def __init__(self, test_list):
679 self.test_list = test_list
Hung-Te Lin96632362012-03-20 21:14:18 +0800680
Jon Salz0697cbf2012-07-04 15:14:04 +0800681 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
Hung-Te Lin96632362012-03-20 21:14:18 +0800689
Jon Salz0697cbf2012-07-04 15:14:04 +0800690 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)
Hung-Te Lin96632362012-03-20 21:14:18 +0800694
Jon Salz0697cbf2012-07-04 15:14:04 +0800695 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'))
Hung-Te Lin96632362012-03-20 21:14:18 +0800700
Jon Salz0697cbf2012-07-04 15:14:04 +0800701 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))
Hung-Te Lin96632362012-03-20 21:14:18 +0800709
Jon Salz0697cbf2012-07-04 15:14:04 +0800710 prompt = 'Review: Test Status Information'
711 if self.notebook.get_n_pages() > 1:
712 prompt += '\nPress left/right to change tabs'
Hung-Te Lin96632362012-03-20 21:14:18 +0800713
Jon Salz0697cbf2012-07-04 15:14:04 +0800714 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
Hung-Te Lin96632362012-03-20 21:14:18 +0800723
724
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800725class TestDirectory(gtk.VBox):
Jon Salz0697cbf2012-07-04 15:14:04 +0800726 '''Widget containing a list of tests, colored by test status.
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800727
Jon Salz0697cbf2012-07-04 15:14:04 +0800728 This is the widget corresponding to the RHS test panel.
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800729
Jon Salz0697cbf2012-07-04 15:14:04 +0800730 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
744 def __init__(self, test_list):
745 gtk.VBox.__init__(self)
746 self.set_spacing(0)
747 self._label_map = {}
748 self._visible_status = []
749 self._shortcut_map = {}
750 self._hard_shortcuts = set(
751 test.kbd_shortcut for test in test_list.walk()
752 if test.kbd_shortcut is not None)
753
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).
778 if test.is_group():
779 gen = (x for x in string.digits if x not in self._shortcut_map)
780 else:
781 gen = (x for x in test.label_en.lower() + string.lowercase
782 if x.isalnum() and x not in self._shortcut_map
783 and x not in self._hard_shortcuts)
784 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]
802
803 if (xevent.state & GLOBAL_HOT_KEY_MASK == GLOBAL_HOT_KEY_MASK):
804 event_type = GLOBAL_HOT_KEY_EVENTS.get(shortcut)
805 if event_type:
806 event_client.post_event(Event(event_type))
807 else:
808 logging.warning('Unbound global hot key %s', key)
809 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))
816 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).
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800830 '''
Jon Salz0697cbf2012-07-04 15:14:04 +0800831 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)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800837
Jon Salz0697cbf2012-07-04 15:14:04 +0800838 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
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800847
Jon Salz0697cbf2012-07-04 15:14:04 +0800848 self._visible_status = new_test_status
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800849
Jon Salz0697cbf2012-07-04 15:14:04 +0800850 new_test_map = dict((t.path, t) for t, s in new_test_status)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800851
Jon Salz0697cbf2012-07-04 15:14:04 +0800852 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)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800857
Jon Salz0697cbf2012-07-04 15:14:04 +0800858 new_tests = new_visible - old_visible
Jon Salz0405ab52012-03-16 15:26:52 +0800859
Jon Salz0697cbf2012-07-04 15:14:04 +0800860 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)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800871
Jon Salz0697cbf2012-07-04 15:14:04 +0800872 self.show_all()
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800873
874
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800875
876class UiState(object):
877
Jon Salz0697cbf2012-07-04 15:14:04 +0800878 WIDGET_NONE = 0
879 WIDGET_IDLE = 1
880 WIDGET_SUMMARY = 2
881 WIDGET_REVIEW = 3
Hung-Te Lin96632362012-03-20 21:14:18 +0800882
Jon Salz0697cbf2012-07-04 15:14:04 +0800883 def __init__(self, test_widget_box, test_directory_widget, test_list):
884 self._test_widget_box = test_widget_box
885 self._test_directory_widget = test_directory_widget
886 self._test_list = test_list
887 self._transition_count = 0
888 self._active_test_label_map = None
889 self._active_widget = self.WIDGET_NONE
890 self.update_test_state()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800891
Jon Salz0697cbf2012-07-04 15:14:04 +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
Hung-Te Lin96632362012-03-20 21:14:18 +0800902
Jon Salz0697cbf2012-07-04 15:14:04 +0800903 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
Hung-Te Lin96632362012-03-20 21:14:18 +0800915
Jon Salz0697cbf2012-07-04 15:14:04 +0800916 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
Hung-Te Lin96632362012-03-20 21:14:18 +0800926
Jon Salz0697cbf2012-07-04 15:14:04 +0800927 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
Hung-Te Lin96632362012-03-20 21:14:18 +0800933
Jon Salz0697cbf2012-07-04 15:14:04 +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
Jon Salz0697cbf2012-07-04 15:14:04 +0800942 def filter_visible_test_state(tests):
943 '''List currently visible tests and their status.
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800944
Jon Salz0697cbf2012-07-04 15:14:04 +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.
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800948
Jon Salz0697cbf2012-07-04 15:14:04 +0800949 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:
955 if test.is_group():
956 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
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800963
Jon Salz0697cbf2012-07-04 15:14:04 +0800964 visible_test_state = filter_visible_test_state(self._test_list.subtests)
965 self._test_directory_widget.update(visible_test_state)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800966
Jon Salz0697cbf2012-07-04 15:14:04 +0800967 if not active_tests:
968 # 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
Jon Salz0697cbf2012-07-04 15:14:04 +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
Jon Salz0697cbf2012-07-04 15:14:04 +0800982 if waiting_for_transition():
983 gobject.timeout_add(_NO_ACTIVE_TEST_DELAY_MS,
984 idle_transition_check,
985 self._transition_count)
986 return
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800987
Jon Salz0697cbf2012-07-04 15:14:04 +0800988 self._transition_count += 1
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800989
Jon Salz0697cbf2012-07-04 15:14:04 +0800990 if any(t.has_ui for t in active_tests):
991 # Remove the widget (if any) since there is an active test
992 # with a UI.
993 self.remove_state_widget()
994 return
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800995
Jon Salz0697cbf2012-07-04 15:14:04 +0800996 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
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001005
Jon Salz0697cbf2012-07-04 15:14:04 +08001006 # No active UI; draw summary of current test states
1007 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):
Jon Salz0697cbf2012-07-04 15:14:04 +08001011 # 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] +
1017 [(GLOBAL_HOT_KEY_MASK, k)
1018 for k in GLOBAL_HOT_KEY_EVENTS] +
1019 [(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):
Jon Salz0697cbf2012-07-04 15:14:04 +08001032 '''Starts a thread to reposition a client window once it appears.
Jon Salz5da61e62012-05-31 13:06:22 +08001033
Jon Salz0697cbf2012-07-04 15:14:04 +08001034 This is useful to avoid blocking the console.
Jon Salz5da61e62012-05-31 13:06:22 +08001035
Jon Salz0697cbf2012-07-04 15:14:04 +08001036 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()
Jon Salz5da61e62012-05-31 13:06:22 +08001055 return
Jon Salz0697cbf2012-07-04 15:14:04 +08001056 # Wait 100 ms and try again.
1057 time.sleep(.1)
1058 thread = threading.Thread(target=reposition)
1059 thread.daemon = True
1060 thread.start()
Jon Salz5da61e62012-05-31 13:06:22 +08001061
1062
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001063def main(test_list_path):
Jon Salz0697cbf2012-07-04 15:14:04 +08001064 '''Starts the main UI.
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001065
Jon Salz0697cbf2012-07-04 15:14:04 +08001066 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.'''
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001069
Jon Salz0697cbf2012-07-04 15:14:04 +08001070 test_list = None
1071 ui_state = None
1072 event_client = None
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001073
Jon Salz0697cbf2012-07-04 15:14:04 +08001074 def handle_key_release_event(_, event):
1075 logging.info('base ui key event (%s)', event.keyval)
1076 return True
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001077
Jon Salz0697cbf2012-07-04 15:14:04 +08001078 def handle_event(event):
1079 if event.type == Event.Type.STATE_CHANGE:
1080 ui_state.update_test_state()
1081 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
Jon Salz0697cbf2012-07-04 15:14:04 +08001085 test_list = factory.read_test_list(test_list_path)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001086
Jon Salz0697cbf2012-07-04 15:14:04 +08001087 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
1088 window.connect('destroy', lambda _: gtk.main_quit())
1089 window.modify_bg(gtk.STATE_NORMAL, BLACK)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001090
Jon Salz0697cbf2012-07-04 15:14:04 +08001091 disp = Display()
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001092
Jon Salz0697cbf2012-07-04 15:14:04 +08001093 event_client = EventClient(
1094 callback=handle_event,
1095 event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001096
Jon Salz0697cbf2012-07-04 15:14:04 +08001097 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)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001102
Jon Salz0697cbf2012-07-04 15:14:04 +08001103 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)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001111
Jon Salz0697cbf2012-07-04 15:14:04 +08001112 test_directory = TestDirectory(test_list)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001113
Jon Salz0697cbf2012-07-04 15:14:04 +08001114 rhs_box = gtk.EventBox()
1115 rhs_box.modify_bg(gtk.STATE_NORMAL, _LABEL_TROUGH_COLOR)
1116 rhs_box.add(test_directory)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001117
Jon Salz0697cbf2012-07-04 15:14:04 +08001118 console_box = gtk.EventBox()
1119 console_box.set_size_request(*convert_pixels((-1, 180)))
1120 console_box.modify_bg(gtk.STATE_NORMAL, BLACK)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001121
Jon Salz0697cbf2012-07-04 15:14:04 +08001122 test_widget_box = gtk.Alignment()
1123 test_widget_box.set_size_request(-1, -1)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001124
Jon Salz0697cbf2012-07-04 15:14:04 +08001125 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)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001129
Jon Salz0697cbf2012-07-04 15:14:04 +08001130 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)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001134
Jon Salz0697cbf2012-07-04 15:14:04 +08001135 window.connect('key-release-event', handle_key_release_event)
1136 window.add_events(gtk.gdk.KEY_RELEASE_MASK)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001137
Jon Salz0697cbf2012-07-04 15:14:04 +08001138 ui_state = UiState(test_widget_box, test_directory, test_list)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001139
Jon Salz0697cbf2012-07-04 15:14:04 +08001140 window.add(base_box)
1141 window.show_all()
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001142
Jon Salz0697cbf2012-07-04 15:14:04 +08001143 grab_shortcut_keys(disp, test_directory.handle_xevent, event_client)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001144
Jon Salz0697cbf2012-07-04 15:14:04 +08001145 hide_cursor(window.window)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001146
Jon Salz0697cbf2012-07-04 15:14:04 +08001147 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)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001151
Jon Salz0697cbf2012-07-04 15:14:04 +08001152 if not factory.in_chroot():
1153 dummy_console = Console(console_box.get_allocation())
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001154
Jon Salz0697cbf2012-07-04 15:14:04 +08001155 event_client.post_event(Event(Event.Type.UI_READY))
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001156
Jon Salz0697cbf2012-07-04 15:14:04 +08001157 logging.info('cros/factory/ui setup done, starting gtk.main()...')
1158 gtk.main()
1159 logging.info('cros/factory/ui gtk.main() finished, exiting.')
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001160
1161
1162if __name__ == '__main__':
Jon Salz0697cbf2012-07-04 15:14:04 +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 Salz0697cbf2012-07-04 15:14:04 +08001169 if len(args) != 1:
1170 parser.error('Incorrect number of arguments')
Jon Salz14bcbb02012-03-17 15:11:50 +08001171
Jon Salz0697cbf2012-07-04 15:14:04 +08001172 factory.init_logging('ui', verbose=options.verbose)
1173 main(sys.argv[1])