blob: 9eda13ef860930c7b6672f67a13968bb2882a01a [file] [log] [blame]
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001#!/usr/bin/python -u
Hung-Te Linf2f78f72012-02-08 19:27:11 +08002# -*- coding: utf-8 -*-
3#
4# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
5# Use of this source code is governed by a BSD-style license that can be
6# found in the LICENSE file.
7
8
9# DESCRIPTION :
10#
11# This library provides convenience routines to launch factory tests.
12# This includes support for drawing the test widget in a window at the
13# proper location, grabbing control of the mouse, and making the mouse
14# cursor disappear.
Hung-Te Lin6bb48552012-02-09 14:37:43 +080015#
16# This UI is intended to be used by the factory autotest suite to
17# provide factory operators feedback on test status and control over
18# execution order.
19#
20# In short, the UI is composed of a 'console' panel on the bottom of
21# the screen which displays the autotest log, and there is also a
22# 'test list' panel on the right hand side of the screen. The
23# majority of the screen is dedicated to tests, which are executed in
24# seperate processes, but instructed to display their own UIs in this
25# dedicated area whenever possible. Tests in the test list are
26# executed in order by default, but can be activated on demand via
27# associated keyboard shortcuts. As tests are run, their status is
28# color-indicated to the operator -- greyed out means untested, yellow
29# means active, green passed and red failed.
Hung-Te Linf2f78f72012-02-08 19:27:11 +080030
Hung-Te Lin6bb48552012-02-09 14:37:43 +080031import logging
32import os
33import re
Tammo Spalinkdb8d7112012-03-13 18:54:37 +080034import string
Hung-Te Lin6bb48552012-02-09 14:37:43 +080035import subprocess
36import sys
Hung-Te Lin96632362012-03-20 21:14:18 +080037from itertools import count, izip, product
Jon Salz14bcbb02012-03-17 15:11:50 +080038from optparse import OptionParser
Hung-Te Linf2f78f72012-02-08 19:27:11 +080039
Hung-Te Lin6bb48552012-02-09 14:37:43 +080040# GTK and X modules
41import gobject
42import gtk
43import pango
44
Tammo Spalinkdb8d7112012-03-13 18:54:37 +080045# Guard loading Xlib because it is currently not available in the
46# image build process host-depends list. Failure to load in
47# production should always manifest during regular use.
48try:
49 from Xlib import X
50 from Xlib.display import Display
51except:
52 pass
53
Hung-Te Lin6bb48552012-02-09 14:37:43 +080054# Factory and autotest modules
Hung-Te Linf2f78f72012-02-08 19:27:11 +080055import factory_common
Hung-Te Linde45e9c2012-03-19 13:02:06 +080056from autotest_lib.client.common_lib import error
Hung-Te Linf2f78f72012-02-08 19:27:11 +080057from autotest_lib.client.cros import factory
58from autotest_lib.client.cros.factory import TestState
59from autotest_lib.client.cros.factory.event import Event, EventClient
60
Hung-Te Lin6bb48552012-02-09 14:37:43 +080061
Hung-Te Linf2f78f72012-02-08 19:27:11 +080062# For compatibility with tests before TestState existed
63ACTIVE = TestState.ACTIVE
64PASSED = TestState.PASSED
65FAILED = TestState.FAILED
66UNTESTED = TestState.UNTESTED
67
Hung-Te Line94e0a02012-03-19 18:20:35 +080068# Arrow symbols
69SYMBOL_RIGHT_ARROW = u'\u25b8'
70SYMBOL_DOWN_ARROW = u'\u25bc'
71
Hung-Te Lin6bb48552012-02-09 14:37:43 +080072# Color definition
Hung-Te Linf2f78f72012-02-08 19:27:11 +080073BLACK = gtk.gdk.Color()
74RED = gtk.gdk.Color(0xFFFF, 0, 0)
75GREEN = gtk.gdk.Color(0, 0xFFFF, 0)
76BLUE = gtk.gdk.Color(0, 0, 0xFFFF)
77WHITE = gtk.gdk.Color(0xFFFF, 0xFFFF, 0xFFFF)
Hung-Te Linf2f78f72012-02-08 19:27:11 +080078LIGHT_GREEN = gtk.gdk.color_parse('light green')
Hung-Te Linf2f78f72012-02-08 19:27:11 +080079SEP_COLOR = gtk.gdk.color_parse('grey50')
80
81RGBA_GREEN_OVERLAY = (0, 0.5, 0, 0.6)
82RGBA_YELLOW_OVERLAY = (0.6, 0.6, 0, 0.6)
Hsinyu Chaoe8584b22012-04-05 17:53:08 +080083RGBA_RED_OVERLAY = (0.5, 0, 0, 0.6)
Hung-Te Linf2f78f72012-02-08 19:27:11 +080084
85LABEL_COLORS = {
86 TestState.ACTIVE: gtk.gdk.color_parse('light goldenrod'),
87 TestState.PASSED: gtk.gdk.color_parse('pale green'),
88 TestState.FAILED: gtk.gdk.color_parse('tomato'),
89 TestState.UNTESTED: gtk.gdk.color_parse('dark slate grey')}
90
91LABEL_FONT = pango.FontDescription('courier new condensed 16')
Hung-Te Linbf545582012-02-15 17:08:07 +080092LABEL_LARGE_FONT = pango.FontDescription('courier new condensed 24')
Hung-Te Linf2f78f72012-02-08 19:27:11 +080093
Jon Salzf81f6082012-03-23 19:37:34 +080094FAIL_TIMEOUT = 60
Hung-Te Linf2f78f72012-02-08 19:27:11 +080095
Hung-Te Line94e0a02012-03-19 18:20:35 +080096MESSAGE_NO_ACTIVE_TESTS = (
97 "No more tests to run. To re-run items, press shortcuts\n"
98 "from the test list in right side or from following list:\n\n"
99 "Ctrl-Alt-A (Auto-Run):\n"
100 " Test remaining untested items.\n\n"
101 "Ctrl-Alt-F (Re-run Failed):\n"
Hung-Te Lin96632362012-03-20 21:14:18 +0800102 " Re-test failed items.\n\n"
Hung-Te Line94e0a02012-03-19 18:20:35 +0800103 "Ctrl-Alt-R (Reset):\n"
104 " Re-test everything.\n\n"
Hung-Te Lin96632362012-03-20 21:14:18 +0800105 "Ctrl-Alt-Z (Information):\n"
106 " Review test results and information.\n\n"
Hung-Te Line94e0a02012-03-19 18:20:35 +0800107 )
108
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800109USER_PASS_FAIL_SELECT_STR = (
110 'hit TAB to fail and ENTER to pass\n' +
111 '錯誤請按 TAB,成功請按 ENTER')
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800112# Resolution where original UI is designed for.
113_UI_SCREEN_WIDTH = 1280
114_UI_SCREEN_HEIGHT = 800
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800115
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800116_LABEL_STATUS_ROW_SIZE = (300, 30)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800117_LABEL_EN_SIZE = (170, 35)
118_LABEL_ZH_SIZE = (70, 35)
119_LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
120_LABEL_ZH_FONT = pango.FontDescription('normal 12')
121_LABEL_T_SIZE = (40, 35)
122_LABEL_T_FONT = pango.FontDescription('arial ultra-condensed 10')
123_LABEL_UNTESTED_FG = gtk.gdk.color_parse('grey40')
124_LABEL_TROUGH_COLOR = gtk.gdk.color_parse('grey20')
125_LABEL_STATUS_SIZE = (140, 30)
126_LABEL_STATUS_FONT = pango.FontDescription(
127 'courier new bold extra-condensed 16')
128_OTHER_LABEL_FONT = pango.FontDescription('courier new condensed 20')
129
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800130_NO_ACTIVE_TEST_DELAY_MS = 500
131
Jon Salz0405ab52012-03-16 15:26:52 +0800132GLOBAL_HOT_KEY_EVENTS = {
133 'r': Event.Type.RESTART_TESTS,
134 'a': Event.Type.AUTO_RUN,
135 'f': Event.Type.RE_RUN_FAILED,
Hung-Te Lin96632362012-03-20 21:14:18 +0800136 'z': Event.Type.REVIEW,
Jon Salz0405ab52012-03-16 15:26:52 +0800137 }
138try:
139 # Works only if X is available.
140 GLOBAL_HOT_KEY_MASK = X.ControlMask | X.Mod1Mask
141except:
142 pass
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800143
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800144# ---------------------------------------------------------------------------
145# Client Library
146
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800147
Hung-Te Lin4b0b61f2012-03-05 14:20:06 +0800148# TODO(hungte) Replace gtk_lock by gtk.gdk.lock when it's availble (need pygtk
149# 2.2x, and we're now pinned by 2.1x)
150class _GtkLock(object):
151 __enter__ = gtk.gdk.threads_enter
152 def __exit__(*ignored):
153 gtk.gdk.threads_leave()
154
155
156gtk_lock = _GtkLock()
157
158
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800159def make_label(message, font=LABEL_FONT, fg=LIGHT_GREEN,
160 size=None, alignment=None):
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800161 """Returns a label widget.
162
163 A wrapper for gtk.Label. The unit of size is pixels under resolution
164 _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
165
166 @param message: A string to be displayed.
167 @param font: Font descriptor for the label.
168 @param fg: Foreground color.
169 @param size: Minimum size for this label.
170 @param alignment: Alignment setting.
171 @return: A label widget.
172 """
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800173 l = gtk.Label(message)
174 l.modify_font(font)
175 l.modify_fg(gtk.STATE_NORMAL, fg)
176 if size:
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800177 # Convert size according to the current resolution.
178 l.set_size_request(*convert_pixels(size))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800179 if alignment:
180 l.set_alignment(*alignment)
181 return l
182
183
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800184def make_status_row(init_prompt,
185 init_status,
186 label_size=_LABEL_STATUS_ROW_SIZE):
187 """Returns a widget that live updates prompt and status in a row.
188
189 Args:
190 init_prompt: The prompt label text.
191 init_status: The status label text.
192 label_size: The desired size of the prompt label and the status label.
193
194 Returns:
195 1) A dict whose content is linked by the widget.
196 2) A widget to render dict content in "prompt: status" format.
197 """
198 display_dict = {}
199 display_dict['prompt'] = init_prompt
200 display_dict['status'] = init_status
201
202 def prompt_label_expose(widget, event):
203 prompt = display_dict['prompt']
204 widget.set_text(prompt)
205
206 def status_label_expose(widget, event):
207 status = display_dict['status']
208 widget.set_text(status)
209 widget.modify_fg(gtk.STATE_NORMAL, LABEL_COLORS[status])
210
211 prompt_label = make_label(
212 init_prompt, size=label_size,
213 alignment=(0, 0.5))
214 delimiter_label = make_label(':', alignment=(0, 0.5))
215 status_label = make_label(
216 init_status, size=label_size,
217 alignment=(0, 0.5), fg=LABEL_COLORS[init_status])
218
219 widget = gtk.HBox()
220 widget.pack_end(status_label, False, False)
221 widget.pack_end(delimiter_label, False, False)
222 widget.pack_end(prompt_label, False, False)
223
224 status_label.connect('expose_event', status_label_expose)
225 prompt_label.connect('expose_event', prompt_label_expose)
226 return display_dict, widget
227
228
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800229def convert_pixels(size):
230 """Converts a pair in pixel that is suitable for current resolution.
231
232 GTK takes pixels as its unit in many function calls. To maintain the
233 consistency of the UI in different resolution, a conversion is required.
234 Take current resolution and (_UI_SCREEN_WIDTH, _UI_SCREEN_HEIGHT) as
235 the original resolution, this function returns a pair of width and height
236 that is converted for current resolution.
237
238 Because pixels in negative usually indicates unspecified, no conversion
239 will be done for negative pixels.
240
241 In addition, the aspect ratio is not maintained in this function.
242
243 Usage Example:
244 width,_ = convert_pixels((20,-1))
245
246 @param size: A pair of pixels that designed under original resolution.
247 @return: A pair of pixels of (width, height) format.
248 Pixels returned are always integer.
249 """
250 return (int(float(size[0]) / _UI_SCREEN_WIDTH * gtk.gdk.screen_width()
251 if (size[0] > 0) else size[0]),
252 int(float(size[1]) / _UI_SCREEN_HEIGHT * gtk.gdk.screen_height()
253 if (size[1] > 0) else size[1]))
254
255
256def make_hsep(height=1):
257 """Returns a widget acts as a horizontal separation line.
258
259 The unit is pixels under resolution _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
260 """
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800261 frame = gtk.EventBox()
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800262 # Convert height according to the current resolution.
263 frame.set_size_request(*convert_pixels((-1, height)))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800264 frame.modify_bg(gtk.STATE_NORMAL, SEP_COLOR)
265 return frame
266
267
268def make_vsep(width=1):
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800269 """Returns a widget acts as a vertical separation line.
270
271 The unit is pixels under resolution _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
272 """
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800273 frame = gtk.EventBox()
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800274 # Convert width according to the current resolution.
275 frame.set_size_request(*convert_pixels((width, -1)))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800276 frame.modify_bg(gtk.STATE_NORMAL, SEP_COLOR)
277 return frame
278
279
Hung-Te Linaf8ec542012-03-14 20:01:40 +0800280def make_countdown_widget(prompt=None, value=None, fg=LIGHT_GREEN):
281 if prompt is None:
282 prompt = 'time remaining / 剩餘時間: '
283 if value is None:
284 value = '%s' % FAIL_TIMEOUT
285 title = make_label(prompt, fg=fg, alignment=(1, 0.5))
286 countdown = make_label(value, fg=fg, alignment=(0, 0.5))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800287 hbox = gtk.HBox()
288 hbox.pack_start(title)
289 hbox.pack_start(countdown)
290 eb = gtk.EventBox()
291 eb.modify_bg(gtk.STATE_NORMAL, BLACK)
292 eb.add(hbox)
293 return eb, countdown
294
295
296def hide_cursor(gdk_window):
297 pixmap = gtk.gdk.Pixmap(None, 1, 1, 1)
298 color = gtk.gdk.Color()
299 cursor = gtk.gdk.Cursor(pixmap, pixmap, color, color, 0, 0)
300 gdk_window.set_cursor(cursor)
301
302
303def calc_scale(wanted_x, wanted_y):
304 (widget_size_x, widget_size_y) = factory.get_shared_data('test_widget_size')
305 scale_x = (0.9 * widget_size_x) / wanted_x
306 scale_y = (0.9 * widget_size_y) / wanted_y
307 scale = scale_y if scale_y < scale_x else scale_x
308 scale = 1 if scale > 1 else scale
309 factory.log('scale: %s' % scale)
310 return scale
311
312
313def trim(text, length):
314 if len(text) > length:
315 text = text[:length-3] + '...'
316 return text
317
318
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800319class InputError(ValueError):
320 """Execption for input window callbacks to change status text message."""
321 pass
322
323
Hung-Te Linbf545582012-02-15 17:08:07 +0800324def make_input_window(prompt=None,
325 init_value=None,
326 msg_invalid=None,
327 font=None,
328 on_validate=None,
329 on_keypress=None,
330 on_complete=None):
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800331 """Creates a widget to prompt user for a valid string.
Hung-Te Linbf545582012-02-15 17:08:07 +0800332
333 @param prompt: A string to be displayed. None for default message.
334 @param init_value: Initial value to be set.
335 @param msg_invalid: Status string to display when input is invalid. None for
336 default message.
337 @param font: Font specification (string or pango.FontDescription) for label
338 and entry. None for default large font.
339 @param on_validate: A callback function to validate if the input from user
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800340 is valid. None for allowing any non-empty input. Any ValueError or
341 ui.InputError raised during execution in on_validate will be displayed
342 in bottom status.
Hung-Te Linbf545582012-02-15 17:08:07 +0800343 @param on_keypress: A callback function when each keystroke is hit.
344 @param on_complete: A callback function when a valid string is passed.
345 None to stop (gtk.main_quit).
Hung-Te Lin7130d9e2012-03-16 15:41:18 +0800346 @return: A widget with prompt, input entry, and status label. To access
347 these elements, use attribute 'prompt', 'entry', and 'label'.
Hung-Te Linbf545582012-02-15 17:08:07 +0800348 """
349 DEFAULT_MSG_INVALID = "Invalid input / 輸入不正確"
350 DEFAULT_PROMPT = "Enter Data / 輸入資料:"
351
352 def enter_callback(entry):
353 text = entry.get_text()
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800354 try:
355 if (on_validate and (not on_validate(text))) or (not text.strip()):
356 raise ValueError(msg_invalid)
Hung-Te Linbf545582012-02-15 17:08:07 +0800357 on_complete(text) if on_complete else gtk.main_quit()
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800358 except ValueError as e:
359 gtk.gdk.beep()
360 status_label.set_text('ERROR: %s' % e.message)
Hung-Te Linbf545582012-02-15 17:08:07 +0800361 return True
362
363 def key_press_callback(entry, key):
364 status_label.set_text('')
365 if on_keypress:
366 return on_keypress(entry, key)
367 return False
368
369 # Populate default parameters
370 if msg_invalid is None:
371 msg_invalid = DEFAULT_MSG_INVALID
372
373 if prompt is None:
374 prompt = DEFAULT_PROMPT
375
376 if font is None:
377 font = LABEL_LARGE_FONT
378 elif not isinstance(font, pango.FontDescription):
379 font = pango.FontDescription(font)
380
381 widget = gtk.VBox()
382 label = make_label(prompt, font=font)
383 status_label = make_label('', font=font)
384 entry = gtk.Entry()
385 entry.modify_font(font)
386 entry.connect("activate", enter_callback)
387 entry.connect("key_press_event", key_press_callback)
388 if init_value:
389 entry.set_text(init_value)
390 widget.modify_bg(gtk.STATE_NORMAL, BLACK)
391 status_label.modify_fg(gtk.STATE_NORMAL, RED)
392 widget.add(label)
393 widget.pack_start(entry)
394 widget.pack_start(status_label)
Chun-ta Lin2e7f44c2012-03-03 07:29:36 +0800395
Hung-Te Lin7130d9e2012-03-16 15:41:18 +0800396 widget.entry = entry
397 widget.status = status_label
398 widget.prompt = label
399
400 # TODO(itspeter) Replace deprecated get_entry by widget.entry.
Chun-ta Lin2e7f44c2012-03-03 07:29:36 +0800401 # Method for getting the entry.
402 widget.get_entry = lambda : entry
Hung-Te Linbf545582012-02-15 17:08:07 +0800403 return widget
404
405
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800406def make_summary_box(tests, state_map, rows=15):
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800407 '''Creates a widget display status of a set of test.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800408
409 @param tests: A list of FactoryTest nodes whose status (and children's
410 status) should be displayed.
411 @param state_map: The state map as provide by the state instance.
412 @param rows: The number of rows to display.
413 @return: A tuple (widget, label_map), where widget is the widget, and
414 label_map is a map from each test to the corresponding label.
415 '''
416 LABEL_EN_SIZE = (170, 35)
417 LABEL_EN_SIZE_2 = (450, 25)
418 LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
419
420 all_tests = sum([list(t.walk(in_order=True)) for t in tests], [])
421 columns = len(all_tests) / rows + (len(all_tests) % rows != 0)
422
423 info_box = gtk.HBox()
424 info_box.set_spacing(20)
425 for status in (TestState.ACTIVE, TestState.PASSED,
426 TestState.FAILED, TestState.UNTESTED):
427 label = make_label(status,
428 size=LABEL_EN_SIZE,
429 font=LABEL_EN_FONT,
430 alignment=(0.5, 0.5),
431 fg=LABEL_COLORS[status])
432 info_box.pack_start(label, False, False)
433
434 vbox = gtk.VBox()
435 vbox.set_spacing(20)
436 vbox.pack_start(info_box, False, False)
437
438 label_map = {}
439
440 if all_tests:
441 status_table = gtk.Table(rows, columns, True)
442 for (j, i), t in izip(product(xrange(columns), xrange(rows)),
443 all_tests):
444 msg_en = ' ' * (t.depth() - 1) + t.label_en
445 msg_en = trim(msg_en, 12)
446 if t.label_zh:
447 msg = '{0:<12} ({1})'.format(msg_en, t.label_zh)
448 else:
449 msg = msg_en
450 status = state_map[t].status
451 status_label = make_label(msg,
452 size=LABEL_EN_SIZE_2,
453 font=LABEL_EN_FONT,
454 alignment=(0.0, 0.5),
455 fg=LABEL_COLORS[status])
456 label_map[t] = status_label
457 status_table.attach(status_label, j, j+1, i, i+1)
458 vbox.pack_start(status_table, False, False)
459
460 return vbox, label_map
461
462
463def run_test_widget(dummy_job, test_widget,
464 invisible_cursor=True,
465 window_registration_callback=None,
466 cleanup_callback=None):
467 test_widget_size = factory.get_shared_data('test_widget_size')
468
469 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
470 window.modify_bg(gtk.STATE_NORMAL, BLACK)
471 window.set_size_request(*test_widget_size)
472
473 def show_window():
474 window.show()
475 window.window.raise_() # pylint: disable=E1101
476 gtk.gdk.pointer_grab(window.window, confine_to=window.window)
477 if invisible_cursor:
478 hide_cursor(window.window)
479
480 test_path = factory.get_current_test_path()
481
482 def handle_event(event):
483 if (event.type == Event.Type.STATE_CHANGE and
484 test_path and event.path == test_path and
485 event.state.visible):
486 show_window()
487
488 event_client = EventClient(
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800489 callback=handle_event, event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800490
491 align = gtk.Alignment(xalign=0.5, yalign=0.5)
492 align.add(test_widget)
493
494 window.add(align)
495 for c in window.get_children():
496 # Show all children, but not the window itself yet.
497 c.show_all()
498
499 if window_registration_callback is not None:
500 window_registration_callback(window)
501
502 # Show the window if it is the visible test, or if the test_path is not
503 # available (e.g., run directly from the command line).
504 if (not test_path) or (
505 TestState.from_dict_or_object(
506 factory.get_state_instance().get_test_state(test_path)).visible):
507 show_window()
508 else:
509 window.hide()
510
Hung-Te Linde45e9c2012-03-19 13:02:06 +0800511 # When gtk.main() is running, it ignores all uncaught exceptions, which is
512 # not preferred by most of our factory tests. To prevent writing special
513 # function raising errors, we hook top level exception handler to always
514 # leave GTK main and raise exception again.
515
516 def exception_hook(exc_type, value, traceback):
517 # Prevent re-entrant.
518 sys.excepthook = old_excepthook
519 session['exception'] = (exc_type, value, traceback)
520 gobject.idle_add(gtk.main_quit)
521 return old_excepthook(exc_type, value, traceback)
522
523 session = {}
524 old_excepthook = sys.excepthook
525 sys.excepthook = exception_hook
526
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800527 gtk.main()
528
529 gtk.gdk.pointer_ungrab()
530
531 if cleanup_callback is not None:
532 cleanup_callback()
533
534 del event_client
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800535
Hung-Te Linde45e9c2012-03-19 13:02:06 +0800536 sys.excepthook = old_excepthook
537 exc_info = session.get('exception')
538 if exc_info is not None:
539 logging.error(exc_info[0], exc_info=exc_info)
540 raise error.TestError(exc_info[1])
541
542
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800543
544# ---------------------------------------------------------------------------
545# Server Implementation
546
547
548class Console(object):
549 '''Display a progress log. Implemented by launching an borderless
550 xterm at a strategic location, and running tail against the log.'''
551
552 def __init__(self, allocation):
Chun-Ta Lin734e96a2012-03-16 20:05:33 +0800553 # Specify how many lines and characters per line are displayed.
554 XTERM_DISPLAY_LINES = 13
555 XTERM_DISPLAY_CHARS = 120
556 # Extra space reserved for pixels between lines.
557 XTERM_RESERVED_LINES = 3
558
559 xterm_coords = '%dx%d+%d+%d' % (XTERM_DISPLAY_CHARS,
560 XTERM_DISPLAY_LINES,
561 allocation.x,
562 allocation.y)
563 xterm_reserved_height = gtk.gdk.screen_height() - allocation.y
564 font_size = int(float(xterm_reserved_height) / (XTERM_DISPLAY_LINES +
565 XTERM_RESERVED_LINES))
566 logging.info('xterm_reserved_height = %d' % xterm_reserved_height)
567 logging.info('font_size = %d' % font_size)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800568 logging.info('xterm_coords = %s', xterm_coords)
569 xterm_opts = ('-bg black -fg lightgray -bw 0 -g %s' % xterm_coords)
Chun-Ta Lin734e96a2012-03-16 20:05:33 +0800570 xterm_cmd = (
571 ['urxvt'] + xterm_opts.split() +
572 ['-fn', 'xft:DejaVu Sans Mono:pixelsize=%s' % font_size] +
573 ['-e', 'bash'] +
574 ['-c', 'tail -f "%s"' % factory.CONSOLE_LOG_PATH])
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800575 logging.info('xterm_cmd = %s', xterm_cmd)
576 self._proc = subprocess.Popen(xterm_cmd)
577
578 def __del__(self):
579 logging.info('console_proc __del__')
580 self._proc.kill()
581
582
583class TestLabelBox(gtk.EventBox): # pylint: disable=R0904
584
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800585 def __init__(self, test):
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800586 gtk.EventBox.__init__(self)
587 self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[TestState.UNTESTED])
Hung-Te Line94e0a02012-03-19 18:20:35 +0800588 self._is_group = test.is_group()
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800589 depth = len(test.get_ancestor_groups())
Hung-Te Line94e0a02012-03-19 18:20:35 +0800590 self._label_text = ' %s%s%s' % (
591 ' ' * depth,
592 SYMBOL_RIGHT_ARROW if self._is_group else ' ',
593 test.label_en)
594 if self._is_group:
595 self._label_text_collapsed = ' %s%s%s' % (
596 ' ' * depth,
597 SYMBOL_DOWN_ARROW if self._is_group else '',
598 test.label_en)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800599 self._label_en = make_label(
Hung-Te Line94e0a02012-03-19 18:20:35 +0800600 self._label_text, size=_LABEL_EN_SIZE,
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800601 font=_LABEL_EN_FONT, alignment=(0, 0.5),
602 fg=_LABEL_UNTESTED_FG)
603 self._label_zh = make_label(
604 test.label_zh, size=_LABEL_ZH_SIZE,
605 font=_LABEL_ZH_FONT, alignment=(0.5, 0.5),
606 fg=_LABEL_UNTESTED_FG)
607 self._label_t = make_label(
608 '', size=_LABEL_T_SIZE, font=_LABEL_T_FONT,
609 alignment=(0.5, 0.5), fg=BLACK)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800610 hbox = gtk.HBox()
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800611 hbox.pack_start(self._label_en, False, False)
612 hbox.pack_start(self._label_zh, False, False)
613 hbox.pack_start(self._label_t, False, False)
614 vbox = gtk.VBox()
615 vbox.pack_start(hbox, False, False)
616 vbox.pack_start(make_hsep(), False, False)
617 self.add(vbox)
618 self._status = None
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800619
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800620 def set_shortcut(self, shortcut):
621 if shortcut is None:
622 return
Hung-Te Lin99f64ef2012-03-22 11:05:45 +0800623 self._label_t.set_text('C-%s' % shortcut.upper())
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800624 attrs = self._label_en.get_attributes() or pango.AttrList()
625 attrs.filter(lambda attr: attr.type == pango.ATTR_UNDERLINE)
626 index_hotkey = self._label_en.get_text().upper().find(shortcut.upper())
627 if index_hotkey != -1:
628 attrs.insert(pango.AttrUnderline(
629 pango.UNDERLINE_LOW, index_hotkey, index_hotkey + 1))
630 attrs.insert(pango.AttrWeight(
631 pango.WEIGHT_BOLD, index_hotkey, index_hotkey + 1))
632 self._label_en.set_attributes(attrs)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800633 self.queue_draw()
634
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800635 def update(self, status):
636 if self._status == status:
637 return
638 self._status = status
639 label_fg = (_LABEL_UNTESTED_FG if status == TestState.UNTESTED
640 else BLACK)
Hung-Te Line94e0a02012-03-19 18:20:35 +0800641 if self._is_group:
642 self._label_en.set_text(
643 self._label_text_collapsed if status == TestState.ACTIVE
644 else self._label_text)
645
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800646 for label in [self._label_en, self._label_zh, self._label_t]:
647 label.modify_fg(gtk.STATE_NORMAL, label_fg)
648 self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[status])
649 self.queue_draw()
650
651
Hung-Te Lin96632362012-03-20 21:14:18 +0800652class ReviewInformation(object):
653
654 LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
655 TAB_BORDER = 20
656
657 def __init__(self, test_list):
658 self.test_list = test_list
659
660 def make_error_tab(self, test, state):
661 msg = '%s (%s)\n%s' % (test.label_en, test.label_zh,
662 str(state.error_msg))
663 label = make_label(msg, font=self.LABEL_EN_FONT, alignment=(0.0, 0.0))
664 label.set_line_wrap(True)
665 frame = gtk.Frame()
666 frame.add(label)
667 return frame
668
669 def make_widget(self):
670 bg_color = gtk.gdk.Color(0x1000, 0x1000, 0x1000)
671 self.notebook = gtk.Notebook()
672 self.notebook.modify_bg(gtk.STATE_NORMAL, bg_color)
673
674 test_list = self.test_list
675 state_map = test_list.get_state_map()
676 tab, _ = make_summary_box([test_list], state_map)
677 tab.set_border_width(self.TAB_BORDER)
678 self.notebook.append_page(tab, make_label('Summary'))
679
680 for i, t in izip(
681 count(1),
682 [t for t in test_list.walk()
683 if state_map[t].status == factory.TestState.FAILED
684 and t.is_leaf()]):
685 tab = self.make_error_tab(t, state_map[t])
686 tab.set_border_width(self.TAB_BORDER)
687 self.notebook.append_page(tab, make_label('#%02d' % i))
688
689 prompt = 'Review: Test Status Information'
690 if self.notebook.get_n_pages() > 1:
691 prompt += '\nPress left/right to change tabs'
692
693 control_label = make_label(prompt, font=self.LABEL_EN_FONT,
694 alignment=(0.5, 0.5))
695 vbox = gtk.VBox()
696 vbox.set_spacing(self.TAB_BORDER)
697 vbox.pack_start(control_label, False, False)
698 vbox.pack_start(self.notebook, False, False)
699 vbox.show_all()
700 vbox.grab_focus = self.notebook.grab_focus
701 return vbox
702
703
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800704class TestDirectory(gtk.VBox):
705 '''Widget containing a list of tests, colored by test status.
706
707 This is the widget corresponding to the RHS test panel.
708
709 Attributes:
710 _label_map: Dict of test path to TestLabelBox objects. Should
711 contain an entry for each test that has been visible at some
712 time.
713 _visible_status: List of (test, status) pairs reflecting the
714 last refresh of the set of visible tests. This is used to
715 rememeber what tests were active, to allow implementation of
716 visual refresh only when new active tests appear.
717 _shortcut_map: Dict of keyboard shortcut key to test path.
718 Tracks the current set of keyboard shortcut mappings for the
719 visible set of tests. This will change when the visible
720 test set changes.
721 '''
722
Tammo Spalinkddae4df2012-03-22 11:50:00 +0800723 def __init__(self, test_list):
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800724 gtk.VBox.__init__(self)
725 self.set_spacing(0)
726 self._label_map = {}
727 self._visible_status = []
728 self._shortcut_map = {}
Tammo Spalinkddae4df2012-03-22 11:50:00 +0800729 self._hard_shortcuts = set(
730 test.kbd_shortcut for test in test_list.walk()
731 if test.kbd_shortcut is not None)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800732
733 def _get_test_label(self, test):
734 if test.path in self._label_map:
735 return self._label_map[test.path]
736 label_box = TestLabelBox(test)
737 self._label_map[test.path] = label_box
738 return label_box
739
740 def _remove_shortcut(self, path):
741 reverse_map = dict((v, k) for k, v in self._shortcut_map.items())
742 if path not in reverse_map:
743 logging.error('Removal of non-present shortcut for %s' % path)
744 return
745 shortcut = reverse_map[path]
746 del self._shortcut_map[shortcut]
747
748 def _add_shortcut(self, test):
749 shortcut = test.kbd_shortcut
750 if shortcut in self._shortcut_map:
751 logging.error('Shortcut %s already in use by %s; cannot apply to %s'
752 % (shortcut, self._shortcut_map[shortcut], test.path))
753 shortcut = None
754 if shortcut is None:
755 # Find a suitable shortcut. For groups, use numbers. For
756 # regular tests, use alpha (letters).
Jon Salz0405ab52012-03-16 15:26:52 +0800757 if test.is_group():
758 gen = (x for x in string.digits if x not in self._shortcut_map)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800759 else:
Jon Salz0405ab52012-03-16 15:26:52 +0800760 gen = (x for x in test.label_en.lower() + string.lowercase
Tammo Spalinkddae4df2012-03-22 11:50:00 +0800761 if x.isalnum() and x not in self._shortcut_map
762 and x not in self._hard_shortcuts)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800763 shortcut = next(gen, None)
764 if shortcut is None:
765 logging.error('Unable to find shortcut for %s' % test.path)
766 return
767 self._shortcut_map[shortcut] = test.path
768 return shortcut
769
770 def handle_xevent(self, dummy_src, dummy_cond,
771 xhandle, keycode_map, event_client):
772 for dummy_i in range(0, xhandle.pending_events()):
773 xevent = xhandle.next_event()
774 if xevent.type != X.KeyPress:
775 continue
776 keycode = xevent.detail
777 if keycode not in keycode_map:
778 logging.warning('Ignoring unknown keycode %r' % keycode)
779 continue
780 shortcut = keycode_map[keycode]
Jon Salz0405ab52012-03-16 15:26:52 +0800781
Hung-Te Lin96632362012-03-20 21:14:18 +0800782 if (xevent.state & GLOBAL_HOT_KEY_MASK == GLOBAL_HOT_KEY_MASK):
Jon Salz0405ab52012-03-16 15:26:52 +0800783 event_type = GLOBAL_HOT_KEY_EVENTS.get(shortcut)
784 if event_type:
785 event_client.post_event(Event(event_type))
786 else:
Jon Salz968e90b2012-03-18 16:12:43 +0800787 logging.warning('Unbound global hot key %s', key)
Jon Salz0405ab52012-03-16 15:26:52 +0800788 else:
789 if shortcut not in self._shortcut_map:
790 logging.warning('Ignoring unbound shortcut %r' % shortcut)
791 continue
792 test_path = self._shortcut_map[shortcut]
793 event_client.post_event(Event(Event.Type.SWITCH_TEST,
794 path=test_path))
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800795 return True
796
797 def update(self, new_test_status):
798 '''Refresh the RHS test list to show current status and active groups.
799
800 Refresh the set of visible tests only when new active tests
801 arise. This avoids visual volatility when switching between
802 tests (intervals where no test is active). Also refresh at
803 initial startup.
804
805 Args:
806 new_test_status: A list of (test, status) tuples. The tests
807 order should match how they should be displayed in the
808 directory (rhs panel).
809 '''
810 old_active = set(t for t, s in self._visible_status
811 if s == TestState.ACTIVE)
812 new_active = set(t for t, s in new_test_status
813 if s == TestState.ACTIVE)
814 new_visible = set(t for t, s in new_test_status)
815 old_visible = set(t for t, s in self._visible_status)
816
817 if old_active and not new_active - old_active:
818 # No new active tests, so do not change the displayed test
819 # set, only update the displayed status for currently
820 # visible tests. Not updating _visible_status allows us
821 # to remember the last set of active tests.
822 for test, _ in self._visible_status:
823 status = test.get_state().status
824 self._label_map[test.path].update(status)
825 return
826
827 self._visible_status = new_test_status
828
829 new_test_map = dict((t.path, t) for t, s in new_test_status)
830
831 for test in old_visible - new_visible:
832 label_box = self._label_map[test.path]
833 logging.debug('removing %s test label' % test.path)
834 self.remove(label_box)
835 self._remove_shortcut(test.path)
836
837 new_tests = new_visible - old_visible
838
839 for position, (test, status) in enumerate(new_test_status):
840 label_box = self._get_test_label(test)
841 if test in new_tests:
842 shortcut = self._add_shortcut(test)
843 label_box = self._get_test_label(test)
844 label_box.set_shortcut(shortcut)
845 logging.debug('adding %s test label (sortcut %r, pos %d)' %
846 (test.path, shortcut, position))
847 self.pack_start(label_box, False, False)
848 self.reorder_child(label_box, position)
849 label_box.update(status)
850
851 self.show_all()
852
853
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800854
855class UiState(object):
856
Hung-Te Lin96632362012-03-20 21:14:18 +0800857 WIDGET_NONE = 0
858 WIDGET_IDLE = 1
859 WIDGET_SUMMARY = 2
860 WIDGET_REVIEW = 3
861
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800862 def __init__(self, test_widget_box, test_directory_widget, test_list):
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800863 self._test_widget_box = test_widget_box
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800864 self._test_directory_widget = test_directory_widget
865 self._test_list = test_list
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800866 self._transition_count = 0
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800867 self._active_test_label_map = None
Hung-Te Lin96632362012-03-20 21:14:18 +0800868 self._active_widget = self.WIDGET_NONE
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800869 self.update_test_state()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800870
Hung-Te Lin96632362012-03-20 21:14:18 +0800871 def show_idle_widget(self):
872 self.remove_state_widget()
873 self._test_widget_box.set(0.5, 0.5, 0.0, 0.0)
874 self._test_widget_box.set_padding(0, 0, 0, 0)
875 label = make_label(MESSAGE_NO_ACTIVE_TESTS,
876 font=_OTHER_LABEL_FONT,
877 alignment=(0.5, 0.5))
878 self._test_widget_box.add(label)
879 self._test_widget_box.show_all()
880 self._active_widget = self.WIDGET_IDLE
881
882 def show_summary_widget(self):
883 self.remove_state_widget()
884 state_map = self._test_list.get_state_map()
885 self._test_widget_box.set(0.5, 0.0, 0.0, 0.0)
886 self._test_widget_box.set_padding(40, 0, 0, 0)
887 vbox, self._active_test_label_map = make_summary_box(
888 [t for t in self._test_list.subtests
889 if state_map[t].status == TestState.ACTIVE],
890 state_map)
891 self._test_widget_box.add(vbox)
892 self._test_widget_box.show_all()
893 self._active_widget = self.WIDGET_SUMMARY
894
895 def show_review_widget(self):
896 self.remove_state_widget()
897 self._review_request = False
898 self._test_widget_box.set(0.5, 0.5, 0.0, 0.0)
899 self._test_widget_box.set_padding(0, 0, 0, 0)
900 widget = ReviewInformation(self._test_list).make_widget()
901 self._test_widget_box.add(widget)
902 self._test_widget_box.show_all()
903 widget.grab_focus()
904 self._active_widget = self.WIDGET_REVIEW
905
906 def remove_state_widget(self):
907 for child in self._test_widget_box.get_children():
908 child.hide()
909 self._test_widget_box.remove(child)
910 self._active_test_label_map = None
911 self._active_widget = self.WIDGET_NONE
912
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800913 def update_test_state(self):
914 state_map = self._test_list.get_state_map()
915 active_tests = set(
916 t for t in self._test_list.walk()
917 if t.is_leaf() and state_map[t].status == TestState.ACTIVE)
918 active_groups = set(g for t in active_tests
919 for g in t.get_ancestor_groups())
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800920
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800921 def filter_visible_test_state(tests):
922 '''List currently visible tests and their status.
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800923
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800924 Visible means currently displayed in the RHS panel.
925 Visiblity is implied by being a top level test or having
926 membership in a group with at least one active test.
927
928 Returns:
929 A list of (test, status) tuples for all visible tests,
930 in the order they should be displayed.
931 '''
932 results = []
933 for test in tests:
Jon Salz0405ab52012-03-16 15:26:52 +0800934 if test.is_group():
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800935 results.append((test, TestState.UNTESTED))
936 if test not in active_groups:
937 continue
938 results += filter_visible_test_state(test.subtests)
939 else:
940 results.append((test, state_map[test].status))
941 return results
942
943 visible_test_state = filter_visible_test_state(self._test_list.subtests)
944 self._test_directory_widget.update(visible_test_state)
945
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800946 if not active_tests:
Hung-Te Lin96632362012-03-20 21:14:18 +0800947 # Display the idle or review information screen.
948 def waiting_for_transition():
949 return (self._active_widget not in
950 [self.WIDGET_REVIEW, self.WIDGET_IDLE])
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800951
Hung-Te Lin96632362012-03-20 21:14:18 +0800952 # For smooth transition between tests, idle widget if activated only
953 # after _NO_ACTIVE_TEST_DELAY_MS without state change.
954 def idle_transition_check(cookie):
955 if (waiting_for_transition() and
956 cookie == self._transition_count):
957 self._transition_count += 1
958 self.show_idle_widget()
959 return False
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800960
Hung-Te Lin96632362012-03-20 21:14:18 +0800961 if waiting_for_transition():
962 gobject.timeout_add(_NO_ACTIVE_TEST_DELAY_MS,
963 idle_transition_check,
964 self._transition_count)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800965 return
966
967 self._transition_count += 1
968
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800969 if any(t.has_ui for t in active_tests):
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800970 # Remove the widget (if any) since there is an active test
971 # with a UI.
Hung-Te Lin96632362012-03-20 21:14:18 +0800972 self.remove_state_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800973 return
974
975 if (self._active_test_label_map is not None and
976 all(t in self._active_test_label_map for t in active_tests)):
977 # All active tests are already present in the summary, so just
978 # update their states.
979 for test, label in self._active_test_label_map.iteritems():
980 label.modify_fg(
981 gtk.STATE_NORMAL,
982 LABEL_COLORS[state_map[test].status])
983 return
984
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800985 # No active UI; draw summary of current test states
Hung-Te Lin96632362012-03-20 21:14:18 +0800986 self.show_summary_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800987
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800988
989def grab_shortcut_keys(disp, event_handler, event_client):
990 # We want to receive KeyPress events
991 root = disp.screen().root
992 root.change_attributes(event_mask = X.KeyPressMask)
993 shortcut_set = set(string.lowercase + string.digits)
994 keycode_map = {}
995 for mod, shortcut in ([(X.ControlMask, k) for k in shortcut_set] +
Jon Salz0405ab52012-03-16 15:26:52 +0800996 [(GLOBAL_HOT_KEY_MASK, k)
997 for k in GLOBAL_HOT_KEY_EVENTS] +
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800998 [(X.Mod1Mask, 'Tab')]): # Mod1 = Alt
999 keysym = gtk.gdk.keyval_from_name(shortcut)
1000 keycode = disp.keysym_to_keycode(keysym)
1001 keycode_map[keycode] = shortcut
1002 root.grab_key(keycode, mod, 1, X.GrabModeAsync, X.GrabModeAsync)
1003 # This flushes the XGrabKey calls to the server.
1004 for dummy_x in range(0, root.display.pending_events()):
1005 root.display.next_event()
1006 gobject.io_add_watch(root.display, gobject.IO_IN, event_handler,
1007 root.display, keycode_map, event_client)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001008
1009
1010def main(test_list_path):
1011 '''Starts the main UI.
1012
1013 This is launched by the autotest/cros/factory/client.
1014 When operators press keyboard shortcuts, the shortcut
1015 value is sent as an event to the control program.'''
1016
1017 test_list = None
1018 ui_state = None
1019 event_client = None
1020
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001021 def handle_key_release_event(_, event):
1022 logging.info('base ui key event (%s)', event.keyval)
1023 return True
1024
1025 def handle_event(event):
1026 if event.type == Event.Type.STATE_CHANGE:
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001027 ui_state.update_test_state()
Hung-Te Lin96632362012-03-20 21:14:18 +08001028 elif event.type == Event.Type.REVIEW:
1029 logging.info("Operator activates review information screen")
1030 ui_state.show_review_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001031
1032 test_list = factory.read_test_list(test_list_path)
1033
1034 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
1035 window.connect('destroy', lambda _: gtk.main_quit())
1036 window.modify_bg(gtk.STATE_NORMAL, BLACK)
1037
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001038 disp = Display()
1039
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001040 event_client = EventClient(
1041 callback=handle_event,
1042 event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
1043
1044 screen = window.get_screen()
1045 if (screen is None):
1046 logging.info('ERROR: communication with the X server is not working, ' +
1047 'could not find a working screen. UI exiting.')
1048 sys.exit(1)
1049
1050 screen_size_str = os.environ.get('CROS_SCREEN_SIZE')
1051 if screen_size_str:
1052 match = re.match(r'^(\d+)x(\d+)$', screen_size_str)
1053 assert match, 'CROS_SCREEN_SIZE should be {width}x{height}'
1054 screen_size = (int(match.group(1)), int(match.group(2)))
1055 else:
1056 screen_size = (screen.get_width(), screen.get_height())
1057 window.set_size_request(*screen_size)
1058
Tammo Spalinkddae4df2012-03-22 11:50:00 +08001059 test_directory = TestDirectory(test_list)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001060
1061 rhs_box = gtk.EventBox()
1062 rhs_box.modify_bg(gtk.STATE_NORMAL, _LABEL_TROUGH_COLOR)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001063 rhs_box.add(test_directory)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001064
1065 console_box = gtk.EventBox()
Chun-Ta Lin734e96a2012-03-16 20:05:33 +08001066 console_box.set_size_request(*convert_pixels((-1, 180)))
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001067 console_box.modify_bg(gtk.STATE_NORMAL, BLACK)
1068
1069 test_widget_box = gtk.Alignment()
1070 test_widget_box.set_size_request(-1, -1)
1071
1072 lhs_box = gtk.VBox()
1073 lhs_box.pack_end(console_box, False, False)
1074 lhs_box.pack_start(test_widget_box)
1075 lhs_box.pack_start(make_hsep(3), False, False)
1076
1077 base_box = gtk.HBox()
1078 base_box.pack_end(rhs_box, False, False)
1079 base_box.pack_end(make_vsep(3), False, False)
1080 base_box.pack_start(lhs_box)
1081
1082 window.connect('key-release-event', handle_key_release_event)
1083 window.add_events(gtk.gdk.KEY_RELEASE_MASK)
1084
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001085 ui_state = UiState(test_widget_box, test_directory, test_list)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001086
1087 window.add(base_box)
1088 window.show_all()
1089
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001090 grab_shortcut_keys(disp, test_directory.handle_xevent, event_client)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001091
1092 hide_cursor(window.window)
1093
1094 test_widget_allocation = test_widget_box.get_allocation()
1095 test_widget_size = (test_widget_allocation.width,
1096 test_widget_allocation.height)
1097 factory.set_shared_data('test_widget_size', test_widget_size)
1098
Jon Salz758e6cc2012-04-03 15:47:07 +08001099 if not factory.in_chroot():
1100 dummy_console = Console(console_box.get_allocation())
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001101
1102 event_client.post_event(Event(Event.Type.UI_READY))
1103
1104 logging.info('cros/factory/ui setup done, starting gtk.main()...')
1105 gtk.main()
1106 logging.info('cros/factory/ui gtk.main() finished, exiting.')
1107
1108
1109if __name__ == '__main__':
Jon Salz14bcbb02012-03-17 15:11:50 +08001110 parser = OptionParser(usage='usage: %prog [options] TEST-LIST-PATH')
1111 parser.add_option('-v', '--verbose', dest='verbose',
1112 action='store_true',
1113 help='Enable debug logging')
1114 (options, args) = parser.parse_args()
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001115
Jon Salz14bcbb02012-03-17 15:11:50 +08001116 if len(args) != 1:
1117 parser.error('Incorrect number of arguments')
1118
1119 factory.init_logging('ui', verbose=options.verbose)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001120 main(sys.argv[1])