blob: 61d7f14510785d8f927644b807818912eaba24b5 [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
Jon Salz36fbbb52012-07-05 13:45:06 +0800504 test_path and event.path == test_path):
505 if event.state.visible:
506 show_window()
507 else:
508 window.hide()
Hung-Te Linde45e9c2012-03-19 13:02:06 +0800509
Jon Salz0697cbf2012-07-04 15:14:04 +0800510 event_client = EventClient(
511 callback=handle_event, event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
Hung-Te Linde45e9c2012-03-19 13:02:06 +0800512
Jon Salz0697cbf2012-07-04 15:14:04 +0800513 align = gtk.Alignment(xalign=0.5, yalign=0.5)
514 align.add(test_widget)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800515
Jon Salz0697cbf2012-07-04 15:14:04 +0800516 window.add(align)
517 for c in window.get_children():
518 # Show all children, but not the window itself yet.
519 c.show_all()
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800520
Jon Salz0697cbf2012-07-04 15:14:04 +0800521 if window_registration_callback is not None:
522 window_registration_callback(window)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800523
Jon Salz0697cbf2012-07-04 15:14:04 +0800524 # Show the window if it is the visible test, or if the test_path is not
525 # available (e.g., run directly from the command line).
526 if (not test_path) or (
527 TestState.from_dict_or_object(
528 factory.get_state_instance().get_test_state(test_path)).visible):
529 show_window()
530 else:
531 window.hide()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800532
Jon Salz0697cbf2012-07-04 15:14:04 +0800533 # When gtk.main() is running, it ignores all uncaught exceptions, which is
534 # not preferred by most of our factory tests. To prevent writing special
535 # function raising errors, we hook top level exception handler to always
536 # leave GTK main and raise exception again.
537
538 def exception_hook(exc_type, value, traceback):
539 # Prevent re-entrant.
Hung-Te Linde45e9c2012-03-19 13:02:06 +0800540 sys.excepthook = old_excepthook
Jon Salz0697cbf2012-07-04 15:14:04 +0800541 session['exception'] = (exc_type, value, traceback)
542 gobject.idle_add(gtk.main_quit)
543 return old_excepthook(exc_type, value, traceback)
544
545 session = {}
546 old_excepthook = sys.excepthook
547 sys.excepthook = exception_hook
548
549 gtk.main()
550
551 if not is_chrome_ui():
552 gtk.gdk.pointer_ungrab()
553
554 if cleanup_callback is not None:
555 cleanup_callback()
556
557 del event_client
558
559 sys.excepthook = old_excepthook
560 exc_info = session.get('exception')
561 if exc_info is not None:
562 logging.error(exc_info[0], exc_info=exc_info)
563 raise FactoryTestFailure(exc_info[1])
Hung-Te Linde45e9c2012-03-19 13:02:06 +0800564
565
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800566
567# ---------------------------------------------------------------------------
568# Server Implementation
569
570
571class Console(object):
Jon Salz0697cbf2012-07-04 15:14:04 +0800572 '''Display a progress log. Implemented by launching an borderless
573 xterm at a strategic location, and running tail against the log.'''
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800574
Jon Salz0697cbf2012-07-04 15:14:04 +0800575 def __init__(self, allocation):
576 # Specify how many lines and characters per line are displayed.
577 XTERM_DISPLAY_LINES = 13
578 XTERM_DISPLAY_CHARS = 120
579 # Extra space reserved for pixels between lines.
580 XTERM_RESERVED_LINES = 3
Chun-Ta Lin734e96a2012-03-16 20:05:33 +0800581
Jon Salz0697cbf2012-07-04 15:14:04 +0800582 xterm_coords = '%dx%d+%d+%d' % (XTERM_DISPLAY_CHARS,
583 XTERM_DISPLAY_LINES,
584 allocation.x,
585 allocation.y)
586 xterm_reserved_height = gtk.gdk.screen_height() - allocation.y
587 font_size = int(float(xterm_reserved_height) / (XTERM_DISPLAY_LINES +
588 XTERM_RESERVED_LINES))
589 logging.info('xterm_reserved_height = %d' % xterm_reserved_height)
590 logging.info('font_size = %d' % font_size)
591 logging.info('xterm_coords = %s', xterm_coords)
592 xterm_opts = ('-bg black -fg lightgray -bw 0 -g %s' % xterm_coords)
593 xterm_cmd = (
594 ['urxvt'] + xterm_opts.split() +
595 ['-fn', 'xft:DejaVu Sans Mono:pixelsize=%s' % font_size] +
596 ['-e', 'bash'] +
597 ['-c', 'tail -f "%s"' % factory.CONSOLE_LOG_PATH])
598 logging.info('xterm_cmd = %s', xterm_cmd)
599 self._proc = subprocess.Popen(xterm_cmd)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800600
Jon Salz0697cbf2012-07-04 15:14:04 +0800601 def __del__(self):
602 logging.info('console_proc __del__')
603 self._proc.kill()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800604
605
Jon Salz0697cbf2012-07-04 15:14:04 +0800606class TestLabelBox(gtk.EventBox): # pylint: disable=R0904
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800607
Jon Salz0697cbf2012-07-04 15:14:04 +0800608 def __init__(self, test):
609 gtk.EventBox.__init__(self)
610 self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[TestState.UNTESTED])
611 self._is_group = test.is_group()
612 depth = len(test.get_ancestor_groups())
613 self._label_text = ' %s%s%s' % (
614 ' ' * depth,
615 SYMBOL_RIGHT_ARROW if self._is_group else ' ',
616 test.label_en)
617 if self._is_group:
618 self._label_text_collapsed = ' %s%s%s' % (
619 ' ' * depth,
620 SYMBOL_DOWN_ARROW if self._is_group else '',
621 test.label_en)
622 self._label_en = make_label(
623 self._label_text, size=_LABEL_EN_SIZE,
624 font=_LABEL_EN_FONT, alignment=(0, 0.5),
625 fg=_LABEL_UNTESTED_FG)
626 self._label_zh = make_label(
627 test.label_zh, size=_LABEL_ZH_SIZE,
628 font=_LABEL_ZH_FONT, alignment=(0.5, 0.5),
629 fg=_LABEL_UNTESTED_FG)
630 self._label_t = make_label(
631 '', size=_LABEL_T_SIZE, font=_LABEL_T_FONT,
632 alignment=(0.5, 0.5), fg=BLACK)
633 hbox = gtk.HBox()
634 hbox.pack_start(self._label_en, False, False)
635 hbox.pack_start(self._label_zh, False, False)
636 hbox.pack_start(self._label_t, False, False)
637 vbox = gtk.VBox()
638 vbox.pack_start(hbox, False, False)
639 vbox.pack_start(make_hsep(), False, False)
640 self.add(vbox)
641 self._status = None
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800642
Jon Salz0697cbf2012-07-04 15:14:04 +0800643 def set_shortcut(self, shortcut):
644 if shortcut is None:
645 return
646 self._label_t.set_text('C-%s' % shortcut.upper())
647 attrs = self._label_en.get_attributes() or pango.AttrList()
648 attrs.filter(lambda attr: attr.type == pango.ATTR_UNDERLINE)
649 index_hotkey = self._label_en.get_text().upper().find(shortcut.upper())
650 if index_hotkey != -1:
651 attrs.insert(pango.AttrUnderline(
652 pango.UNDERLINE_LOW, index_hotkey, index_hotkey + 1))
653 attrs.insert(pango.AttrWeight(
654 pango.WEIGHT_BOLD, index_hotkey, index_hotkey + 1))
655 self._label_en.set_attributes(attrs)
656 self.queue_draw()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800657
Jon Salz0697cbf2012-07-04 15:14:04 +0800658 def update(self, status):
659 if self._status == status:
660 return
661 self._status = status
662 label_fg = (_LABEL_UNTESTED_FG if status == TestState.UNTESTED
663 else BLACK)
664 if self._is_group:
665 self._label_en.set_text(
666 self._label_text_collapsed if status == TestState.ACTIVE
667 else self._label_text)
Hung-Te Line94e0a02012-03-19 18:20:35 +0800668
Jon Salz0697cbf2012-07-04 15:14:04 +0800669 for label in [self._label_en, self._label_zh, self._label_t]:
670 label.modify_fg(gtk.STATE_NORMAL, label_fg)
671 self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[status])
672 self.queue_draw()
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800673
674
Hung-Te Lin96632362012-03-20 21:14:18 +0800675class ReviewInformation(object):
676
Jon Salz0697cbf2012-07-04 15:14:04 +0800677 LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
678 TAB_BORDER = 20
Hung-Te Lin96632362012-03-20 21:14:18 +0800679
Jon Salz0697cbf2012-07-04 15:14:04 +0800680 def __init__(self, test_list):
681 self.test_list = test_list
Hung-Te Lin96632362012-03-20 21:14:18 +0800682
Jon Salz0697cbf2012-07-04 15:14:04 +0800683 def make_error_tab(self, test, state):
684 msg = '%s (%s)\n%s' % (test.label_en, test.label_zh,
685 str(state.error_msg))
686 label = make_label(msg, font=self.LABEL_EN_FONT, alignment=(0.0, 0.0))
687 label.set_line_wrap(True)
688 frame = gtk.Frame()
689 frame.add(label)
690 return frame
Hung-Te Lin96632362012-03-20 21:14:18 +0800691
Jon Salz0697cbf2012-07-04 15:14:04 +0800692 def make_widget(self):
693 bg_color = gtk.gdk.Color(0x1000, 0x1000, 0x1000)
694 self.notebook = gtk.Notebook()
695 self.notebook.modify_bg(gtk.STATE_NORMAL, bg_color)
Hung-Te Lin96632362012-03-20 21:14:18 +0800696
Jon Salz0697cbf2012-07-04 15:14:04 +0800697 test_list = self.test_list
698 state_map = test_list.get_state_map()
699 tab, _ = make_summary_box([test_list], state_map)
700 tab.set_border_width(self.TAB_BORDER)
701 self.notebook.append_page(tab, make_label('Summary'))
Hung-Te Lin96632362012-03-20 21:14:18 +0800702
Jon Salz0697cbf2012-07-04 15:14:04 +0800703 for i, t in izip(
704 count(1),
705 [t for t in test_list.walk()
706 if state_map[t].status == factory.TestState.FAILED
707 and t.is_leaf()]):
708 tab = self.make_error_tab(t, state_map[t])
709 tab.set_border_width(self.TAB_BORDER)
710 self.notebook.append_page(tab, make_label('#%02d' % i))
Hung-Te Lin96632362012-03-20 21:14:18 +0800711
Jon Salz0697cbf2012-07-04 15:14:04 +0800712 prompt = 'Review: Test Status Information'
713 if self.notebook.get_n_pages() > 1:
714 prompt += '\nPress left/right to change tabs'
Hung-Te Lin96632362012-03-20 21:14:18 +0800715
Jon Salz0697cbf2012-07-04 15:14:04 +0800716 control_label = make_label(prompt, font=self.LABEL_EN_FONT,
717 alignment=(0.5, 0.5))
718 vbox = gtk.VBox()
719 vbox.set_spacing(self.TAB_BORDER)
720 vbox.pack_start(control_label, False, False)
721 vbox.pack_start(self.notebook, False, False)
722 vbox.show_all()
723 vbox.grab_focus = self.notebook.grab_focus
724 return vbox
Hung-Te Lin96632362012-03-20 21:14:18 +0800725
726
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800727class TestDirectory(gtk.VBox):
Jon Salz0697cbf2012-07-04 15:14:04 +0800728 '''Widget containing a list of tests, colored by test status.
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800729
Jon Salz0697cbf2012-07-04 15:14:04 +0800730 This is the widget corresponding to the RHS test panel.
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800731
Jon Salz0697cbf2012-07-04 15:14:04 +0800732 Attributes:
733 _label_map: Dict of test path to TestLabelBox objects. Should
734 contain an entry for each test that has been visible at some
735 time.
736 _visible_status: List of (test, status) pairs reflecting the
737 last refresh of the set of visible tests. This is used to
738 rememeber what tests were active, to allow implementation of
739 visual refresh only when new active tests appear.
740 _shortcut_map: Dict of keyboard shortcut key to test path.
741 Tracks the current set of keyboard shortcut mappings for the
742 visible set of tests. This will change when the visible
743 test set changes.
744 '''
745
746 def __init__(self, test_list):
747 gtk.VBox.__init__(self)
748 self.set_spacing(0)
749 self._label_map = {}
750 self._visible_status = []
751 self._shortcut_map = {}
752 self._hard_shortcuts = set(
753 test.kbd_shortcut for test in test_list.walk()
754 if test.kbd_shortcut is not None)
755
756 def _get_test_label(self, test):
757 if test.path in self._label_map:
758 return self._label_map[test.path]
759 label_box = TestLabelBox(test)
760 self._label_map[test.path] = label_box
761 return label_box
762
763 def _remove_shortcut(self, path):
764 reverse_map = dict((v, k) for k, v in self._shortcut_map.items())
765 if path not in reverse_map:
766 logging.error('Removal of non-present shortcut for %s' % path)
767 return
768 shortcut = reverse_map[path]
769 del self._shortcut_map[shortcut]
770
771 def _add_shortcut(self, test):
772 shortcut = test.kbd_shortcut
773 if shortcut in self._shortcut_map:
774 logging.error('Shortcut %s already in use by %s; cannot apply to %s'
775 % (shortcut, self._shortcut_map[shortcut], test.path))
776 shortcut = None
777 if shortcut is None:
778 # Find a suitable shortcut. For groups, use numbers. For
779 # regular tests, use alpha (letters).
780 if test.is_group():
781 gen = (x for x in string.digits if x not in self._shortcut_map)
782 else:
783 gen = (x for x in test.label_en.lower() + string.lowercase
784 if x.isalnum() and x not in self._shortcut_map
785 and x not in self._hard_shortcuts)
786 shortcut = next(gen, None)
787 if shortcut is None:
788 logging.error('Unable to find shortcut for %s' % test.path)
789 return
790 self._shortcut_map[shortcut] = test.path
791 return shortcut
792
793 def handle_xevent(self, dummy_src, dummy_cond,
794 xhandle, keycode_map, event_client):
795 for dummy_i in range(0, xhandle.pending_events()):
796 xevent = xhandle.next_event()
797 if xevent.type != X.KeyPress:
798 continue
799 keycode = xevent.detail
800 if keycode not in keycode_map:
801 logging.warning('Ignoring unknown keycode %r' % keycode)
802 continue
803 shortcut = keycode_map[keycode]
804
805 if (xevent.state & GLOBAL_HOT_KEY_MASK == GLOBAL_HOT_KEY_MASK):
806 event_type = GLOBAL_HOT_KEY_EVENTS.get(shortcut)
807 if event_type:
808 event_client.post_event(Event(event_type))
809 else:
810 logging.warning('Unbound global hot key %s', key)
811 else:
812 if shortcut not in self._shortcut_map:
813 logging.warning('Ignoring unbound shortcut %r' % shortcut)
814 continue
815 test_path = self._shortcut_map[shortcut]
816 event_client.post_event(Event(Event.Type.SWITCH_TEST,
817 path=test_path))
818 return True
819
820 def update(self, new_test_status):
821 '''Refresh the RHS test list to show current status and active groups.
822
823 Refresh the set of visible tests only when new active tests
824 arise. This avoids visual volatility when switching between
825 tests (intervals where no test is active). Also refresh at
826 initial startup.
827
828 Args:
829 new_test_status: A list of (test, status) tuples. The tests
830 order should match how they should be displayed in the
831 directory (rhs panel).
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800832 '''
Jon Salz0697cbf2012-07-04 15:14:04 +0800833 old_active = set(t for t, s in self._visible_status
834 if s == TestState.ACTIVE)
835 new_active = set(t for t, s in new_test_status
836 if s == TestState.ACTIVE)
837 new_visible = set(t for t, s in new_test_status)
838 old_visible = set(t for t, s in self._visible_status)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800839
Jon Salz0697cbf2012-07-04 15:14:04 +0800840 if old_active and not new_active - old_active:
841 # No new active tests, so do not change the displayed test
842 # set, only update the displayed status for currently
843 # visible tests. Not updating _visible_status allows us
844 # to remember the last set of active tests.
845 for test, _ in self._visible_status:
846 status = test.get_state().status
847 self._label_map[test.path].update(status)
848 return
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800849
Jon Salz0697cbf2012-07-04 15:14:04 +0800850 self._visible_status = new_test_status
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800851
Jon Salz0697cbf2012-07-04 15:14:04 +0800852 new_test_map = dict((t.path, t) for t, s in new_test_status)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800853
Jon Salz0697cbf2012-07-04 15:14:04 +0800854 for test in old_visible - new_visible:
855 label_box = self._label_map[test.path]
856 logging.debug('removing %s test label' % test.path)
857 self.remove(label_box)
858 self._remove_shortcut(test.path)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800859
Jon Salz0697cbf2012-07-04 15:14:04 +0800860 new_tests = new_visible - old_visible
Jon Salz0405ab52012-03-16 15:26:52 +0800861
Jon Salz0697cbf2012-07-04 15:14:04 +0800862 for position, (test, status) in enumerate(new_test_status):
863 label_box = self._get_test_label(test)
864 if test in new_tests:
865 shortcut = self._add_shortcut(test)
866 label_box = self._get_test_label(test)
867 label_box.set_shortcut(shortcut)
868 logging.debug('adding %s test label (sortcut %r, pos %d)' %
869 (test.path, shortcut, position))
870 self.pack_start(label_box, False, False)
871 self.reorder_child(label_box, position)
872 label_box.update(status)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800873
Jon Salz0697cbf2012-07-04 15:14:04 +0800874 self.show_all()
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800875
876
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800877
878class UiState(object):
879
Jon Salz0697cbf2012-07-04 15:14:04 +0800880 WIDGET_NONE = 0
881 WIDGET_IDLE = 1
882 WIDGET_SUMMARY = 2
883 WIDGET_REVIEW = 3
Hung-Te Lin96632362012-03-20 21:14:18 +0800884
Jon Salz0697cbf2012-07-04 15:14:04 +0800885 def __init__(self, test_widget_box, test_directory_widget, test_list):
886 self._test_widget_box = test_widget_box
887 self._test_directory_widget = test_directory_widget
888 self._test_list = test_list
889 self._transition_count = 0
890 self._active_test_label_map = None
891 self._active_widget = self.WIDGET_NONE
892 self.update_test_state()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800893
Jon Salz0697cbf2012-07-04 15:14:04 +0800894 def show_idle_widget(self):
895 self.remove_state_widget()
896 self._test_widget_box.set(0.5, 0.5, 0.0, 0.0)
897 self._test_widget_box.set_padding(0, 0, 0, 0)
898 label = make_label(MESSAGE_NO_ACTIVE_TESTS,
899 font=_OTHER_LABEL_FONT,
900 alignment=(0.5, 0.5))
901 self._test_widget_box.add(label)
902 self._test_widget_box.show_all()
903 self._active_widget = self.WIDGET_IDLE
Hung-Te Lin96632362012-03-20 21:14:18 +0800904
Jon Salz0697cbf2012-07-04 15:14:04 +0800905 def show_summary_widget(self):
906 self.remove_state_widget()
907 state_map = self._test_list.get_state_map()
908 self._test_widget_box.set(0.5, 0.0, 0.0, 0.0)
909 self._test_widget_box.set_padding(40, 0, 0, 0)
910 vbox, self._active_test_label_map = make_summary_box(
911 [t for t in self._test_list.subtests
912 if state_map[t].status == TestState.ACTIVE],
913 state_map)
914 self._test_widget_box.add(vbox)
915 self._test_widget_box.show_all()
916 self._active_widget = self.WIDGET_SUMMARY
Hung-Te Lin96632362012-03-20 21:14:18 +0800917
Jon Salz0697cbf2012-07-04 15:14:04 +0800918 def show_review_widget(self):
919 self.remove_state_widget()
920 self._review_request = False
921 self._test_widget_box.set(0.5, 0.5, 0.0, 0.0)
922 self._test_widget_box.set_padding(0, 0, 0, 0)
923 widget = ReviewInformation(self._test_list).make_widget()
924 self._test_widget_box.add(widget)
925 self._test_widget_box.show_all()
926 widget.grab_focus()
927 self._active_widget = self.WIDGET_REVIEW
Hung-Te Lin96632362012-03-20 21:14:18 +0800928
Jon Salz0697cbf2012-07-04 15:14:04 +0800929 def remove_state_widget(self):
930 for child in self._test_widget_box.get_children():
931 child.hide()
932 self._test_widget_box.remove(child)
933 self._active_test_label_map = None
934 self._active_widget = self.WIDGET_NONE
Hung-Te Lin96632362012-03-20 21:14:18 +0800935
Jon Salz0697cbf2012-07-04 15:14:04 +0800936 def update_test_state(self):
937 state_map = self._test_list.get_state_map()
938 active_tests = set(
939 t for t in self._test_list.walk()
940 if t.is_leaf() and state_map[t].status == TestState.ACTIVE)
941 active_groups = set(g for t in active_tests
942 for g in t.get_ancestor_groups())
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800943
Jon Salz0697cbf2012-07-04 15:14:04 +0800944 def filter_visible_test_state(tests):
945 '''List currently visible tests and their status.
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800946
Jon Salz0697cbf2012-07-04 15:14:04 +0800947 Visible means currently displayed in the RHS panel.
948 Visiblity is implied by being a top level test or having
949 membership in a group with at least one active test.
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800950
Jon Salz0697cbf2012-07-04 15:14:04 +0800951 Returns:
952 A list of (test, status) tuples for all visible tests,
953 in the order they should be displayed.
954 '''
955 results = []
956 for test in tests:
957 if test.is_group():
958 results.append((test, TestState.UNTESTED))
959 if test not in active_groups:
960 continue
961 results += filter_visible_test_state(test.subtests)
962 else:
963 results.append((test, state_map[test].status))
964 return results
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800965
Jon Salz0697cbf2012-07-04 15:14:04 +0800966 visible_test_state = filter_visible_test_state(self._test_list.subtests)
967 self._test_directory_widget.update(visible_test_state)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800968
Jon Salz0697cbf2012-07-04 15:14:04 +0800969 if not active_tests:
970 # Display the idle or review information screen.
971 def waiting_for_transition():
972 return (self._active_widget not in
973 [self.WIDGET_REVIEW, self.WIDGET_IDLE])
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800974
Jon Salz0697cbf2012-07-04 15:14:04 +0800975 # For smooth transition between tests, idle widget if activated only
976 # after _NO_ACTIVE_TEST_DELAY_MS without state change.
977 def idle_transition_check(cookie):
978 if (waiting_for_transition() and
979 cookie == self._transition_count):
980 self._transition_count += 1
981 self.show_idle_widget()
982 return False
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800983
Jon Salz0697cbf2012-07-04 15:14:04 +0800984 if waiting_for_transition():
985 gobject.timeout_add(_NO_ACTIVE_TEST_DELAY_MS,
986 idle_transition_check,
987 self._transition_count)
988 return
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800989
Jon Salz0697cbf2012-07-04 15:14:04 +0800990 self._transition_count += 1
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800991
Jon Salz0697cbf2012-07-04 15:14:04 +0800992 if any(t.has_ui for t in active_tests):
993 # Remove the widget (if any) since there is an active test
994 # with a UI.
995 self.remove_state_widget()
996 return
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800997
Jon Salz0697cbf2012-07-04 15:14:04 +0800998 if (self._active_test_label_map is not None and
999 all(t in self._active_test_label_map for t in active_tests)):
1000 # All active tests are already present in the summary, so just
1001 # update their states.
1002 for test, label in self._active_test_label_map.iteritems():
1003 label.modify_fg(
1004 gtk.STATE_NORMAL,
1005 LABEL_COLORS[state_map[test].status])
1006 return
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001007
Jon Salz0697cbf2012-07-04 15:14:04 +08001008 # No active UI; draw summary of current test states
1009 self.show_summary_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001010
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001011
1012def grab_shortcut_keys(disp, event_handler, event_client):
Jon Salz0697cbf2012-07-04 15:14:04 +08001013 # We want to receive KeyPress events
1014 root = disp.screen().root
1015 root.change_attributes(event_mask = X.KeyPressMask)
1016 shortcut_set = set(string.lowercase + string.digits)
1017 keycode_map = {}
1018 for mod, shortcut in ([(X.ControlMask, k) for k in shortcut_set] +
1019 [(GLOBAL_HOT_KEY_MASK, k)
1020 for k in GLOBAL_HOT_KEY_EVENTS] +
1021 [(X.Mod1Mask, 'Tab')]): # Mod1 = Alt
1022 keysym = gtk.gdk.keyval_from_name(shortcut)
1023 keycode = disp.keysym_to_keycode(keysym)
1024 keycode_map[keycode] = shortcut
1025 root.grab_key(keycode, mod, 1, X.GrabModeAsync, X.GrabModeAsync)
1026 # This flushes the XGrabKey calls to the server.
1027 for dummy_x in range(0, root.display.pending_events()):
1028 root.display.next_event()
1029 gobject.io_add_watch(root.display, gobject.IO_IN, event_handler,
1030 root.display, keycode_map, event_client)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001031
1032
Jon Salz5da61e62012-05-31 13:06:22 +08001033def start_reposition_thread(title_regexp):
Jon Salz0697cbf2012-07-04 15:14:04 +08001034 '''Starts a thread to reposition a client window once it appears.
Jon Salz5da61e62012-05-31 13:06:22 +08001035
Jon Salz0697cbf2012-07-04 15:14:04 +08001036 This is useful to avoid blocking the console.
Jon Salz5da61e62012-05-31 13:06:22 +08001037
Jon Salz0697cbf2012-07-04 15:14:04 +08001038 Args:
1039 title_regexp: A regexp for the window's title (used to find the
1040 window to reposition).
1041 '''
1042 test_widget_position = (
1043 factory.get_shared_data('test_widget_position'))
1044 if not test_widget_position:
1045 return
1046
1047 def reposition():
1048 display = Display()
1049 root = display.screen().root
1050 for i in xrange(50):
1051 wins = [win for win in root.query_tree().children
1052 if re.match(title_regexp, win.get_wm_name())]
1053 if wins:
1054 wins[0].configure(x=test_widget_position[0],
1055 y=test_widget_position[1])
1056 display.sync()
Jon Salz5da61e62012-05-31 13:06:22 +08001057 return
Jon Salz0697cbf2012-07-04 15:14:04 +08001058 # Wait 100 ms and try again.
1059 time.sleep(.1)
1060 thread = threading.Thread(target=reposition)
1061 thread.daemon = True
1062 thread.start()
Jon Salz5da61e62012-05-31 13:06:22 +08001063
1064
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001065def main(test_list_path):
Jon Salz0697cbf2012-07-04 15:14:04 +08001066 '''Starts the main UI.
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001067
Jon Salz0697cbf2012-07-04 15:14:04 +08001068 This is launched by the autotest/cros/factory/client.
1069 When operators press keyboard shortcuts, the shortcut
1070 value is sent as an event to the control program.'''
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001071
Jon Salz0697cbf2012-07-04 15:14:04 +08001072 test_list = None
1073 ui_state = None
1074 event_client = None
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001075
Jon Salz0697cbf2012-07-04 15:14:04 +08001076 def handle_key_release_event(_, event):
1077 logging.info('base ui key event (%s)', event.keyval)
1078 return True
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001079
Jon Salz0697cbf2012-07-04 15:14:04 +08001080 def handle_event(event):
1081 if event.type == Event.Type.STATE_CHANGE:
1082 ui_state.update_test_state()
1083 elif event.type == Event.Type.REVIEW:
1084 logging.info("Operator activates review information screen")
1085 ui_state.show_review_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001086
Jon Salz0697cbf2012-07-04 15:14:04 +08001087 test_list = factory.read_test_list(test_list_path)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001088
Jon Salz0697cbf2012-07-04 15:14:04 +08001089 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
1090 window.connect('destroy', lambda _: gtk.main_quit())
1091 window.modify_bg(gtk.STATE_NORMAL, BLACK)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001092
Jon Salz0697cbf2012-07-04 15:14:04 +08001093 disp = Display()
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001094
Jon Salz0697cbf2012-07-04 15:14:04 +08001095 event_client = EventClient(
1096 callback=handle_event,
1097 event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001098
Jon Salz0697cbf2012-07-04 15:14:04 +08001099 screen = window.get_screen()
1100 if (screen is None):
1101 logging.info('ERROR: communication with the X server is not working, ' +
1102 'could not find a working screen. UI exiting.')
1103 sys.exit(1)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001104
Jon Salz0697cbf2012-07-04 15:14:04 +08001105 screen_size_str = os.environ.get('CROS_SCREEN_SIZE')
1106 if screen_size_str:
1107 match = re.match(r'^(\d+)x(\d+)$', screen_size_str)
1108 assert match, 'CROS_SCREEN_SIZE should be {width}x{height}'
1109 screen_size = (int(match.group(1)), int(match.group(2)))
1110 else:
1111 screen_size = (screen.get_width(), screen.get_height())
1112 window.set_size_request(*screen_size)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001113
Jon Salz0697cbf2012-07-04 15:14:04 +08001114 test_directory = TestDirectory(test_list)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001115
Jon Salz0697cbf2012-07-04 15:14:04 +08001116 rhs_box = gtk.EventBox()
1117 rhs_box.modify_bg(gtk.STATE_NORMAL, _LABEL_TROUGH_COLOR)
1118 rhs_box.add(test_directory)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001119
Jon Salz0697cbf2012-07-04 15:14:04 +08001120 console_box = gtk.EventBox()
1121 console_box.set_size_request(*convert_pixels((-1, 180)))
1122 console_box.modify_bg(gtk.STATE_NORMAL, BLACK)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001123
Jon Salz0697cbf2012-07-04 15:14:04 +08001124 test_widget_box = gtk.Alignment()
1125 test_widget_box.set_size_request(-1, -1)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001126
Jon Salz0697cbf2012-07-04 15:14:04 +08001127 lhs_box = gtk.VBox()
1128 lhs_box.pack_end(console_box, False, False)
1129 lhs_box.pack_start(test_widget_box)
1130 lhs_box.pack_start(make_hsep(3), False, False)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001131
Jon Salz0697cbf2012-07-04 15:14:04 +08001132 base_box = gtk.HBox()
1133 base_box.pack_end(rhs_box, False, False)
1134 base_box.pack_end(make_vsep(3), False, False)
1135 base_box.pack_start(lhs_box)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001136
Jon Salz0697cbf2012-07-04 15:14:04 +08001137 window.connect('key-release-event', handle_key_release_event)
1138 window.add_events(gtk.gdk.KEY_RELEASE_MASK)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001139
Jon Salz0697cbf2012-07-04 15:14:04 +08001140 ui_state = UiState(test_widget_box, test_directory, test_list)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001141
Jon Salz0697cbf2012-07-04 15:14:04 +08001142 window.add(base_box)
1143 window.show_all()
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001144
Jon Salz0697cbf2012-07-04 15:14:04 +08001145 grab_shortcut_keys(disp, test_directory.handle_xevent, event_client)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001146
Jon Salz0697cbf2012-07-04 15:14:04 +08001147 hide_cursor(window.window)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001148
Jon Salz0697cbf2012-07-04 15:14:04 +08001149 test_widget_allocation = test_widget_box.get_allocation()
1150 test_widget_size = (test_widget_allocation.width,
1151 test_widget_allocation.height)
1152 factory.set_shared_data('test_widget_size', test_widget_size)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001153
Jon Salz0697cbf2012-07-04 15:14:04 +08001154 if not factory.in_chroot():
1155 dummy_console = Console(console_box.get_allocation())
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001156
Jon Salz0697cbf2012-07-04 15:14:04 +08001157 event_client.post_event(Event(Event.Type.UI_READY))
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001158
Jon Salz0697cbf2012-07-04 15:14:04 +08001159 logging.info('cros/factory/ui setup done, starting gtk.main()...')
1160 gtk.main()
1161 logging.info('cros/factory/ui gtk.main() finished, exiting.')
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001162
1163
1164if __name__ == '__main__':
Jon Salz0697cbf2012-07-04 15:14:04 +08001165 parser = OptionParser(usage='usage: %prog [options] TEST-LIST-PATH')
1166 parser.add_option('-v', '--verbose', dest='verbose',
1167 action='store_true',
1168 help='Enable debug logging')
1169 (options, args) = parser.parse_args()
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001170
Jon Salz0697cbf2012-07-04 15:14:04 +08001171 if len(args) != 1:
1172 parser.error('Incorrect number of arguments')
Jon Salz14bcbb02012-03-17 15:11:50 +08001173
Jon Salz0697cbf2012-07-04 15:14:04 +08001174 factory.init_logging('ui', verbose=options.verbose)
1175 main(sys.argv[1])