blob: 1c3f84eeafea0e8a1c61cbb15ee9328be4bcb7a1 [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)
83
84LABEL_COLORS = {
85 TestState.ACTIVE: gtk.gdk.color_parse('light goldenrod'),
86 TestState.PASSED: gtk.gdk.color_parse('pale green'),
87 TestState.FAILED: gtk.gdk.color_parse('tomato'),
88 TestState.UNTESTED: gtk.gdk.color_parse('dark slate grey')}
89
90LABEL_FONT = pango.FontDescription('courier new condensed 16')
Hung-Te Linbf545582012-02-15 17:08:07 +080091LABEL_LARGE_FONT = pango.FontDescription('courier new condensed 24')
Hung-Te Linf2f78f72012-02-08 19:27:11 +080092
Jon Salzf81f6082012-03-23 19:37:34 +080093FAIL_TIMEOUT = 60
Hung-Te Linf2f78f72012-02-08 19:27:11 +080094
Hung-Te Line94e0a02012-03-19 18:20:35 +080095MESSAGE_NO_ACTIVE_TESTS = (
96 "No more tests to run. To re-run items, press shortcuts\n"
97 "from the test list in right side or from following list:\n\n"
98 "Ctrl-Alt-A (Auto-Run):\n"
99 " Test remaining untested items.\n\n"
100 "Ctrl-Alt-F (Re-run Failed):\n"
Hung-Te Lin96632362012-03-20 21:14:18 +0800101 " Re-test failed items.\n\n"
Hung-Te Line94e0a02012-03-19 18:20:35 +0800102 "Ctrl-Alt-R (Reset):\n"
103 " Re-test everything.\n\n"
Hung-Te Lin96632362012-03-20 21:14:18 +0800104 "Ctrl-Alt-Z (Information):\n"
105 " Review test results and information.\n\n"
Hung-Te Line94e0a02012-03-19 18:20:35 +0800106 )
107
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800108USER_PASS_FAIL_SELECT_STR = (
109 'hit TAB to fail and ENTER to pass\n' +
110 '錯誤請按 TAB,成功請按 ENTER')
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800111# Resolution where original UI is designed for.
112_UI_SCREEN_WIDTH = 1280
113_UI_SCREEN_HEIGHT = 800
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800114
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800115_LABEL_STATUS_ROW_SIZE = (300, 30)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800116_LABEL_EN_SIZE = (170, 35)
117_LABEL_ZH_SIZE = (70, 35)
118_LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
119_LABEL_ZH_FONT = pango.FontDescription('normal 12')
120_LABEL_T_SIZE = (40, 35)
121_LABEL_T_FONT = pango.FontDescription('arial ultra-condensed 10')
122_LABEL_UNTESTED_FG = gtk.gdk.color_parse('grey40')
123_LABEL_TROUGH_COLOR = gtk.gdk.color_parse('grey20')
124_LABEL_STATUS_SIZE = (140, 30)
125_LABEL_STATUS_FONT = pango.FontDescription(
126 'courier new bold extra-condensed 16')
127_OTHER_LABEL_FONT = pango.FontDescription('courier new condensed 20')
128
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800129_NO_ACTIVE_TEST_DELAY_MS = 500
130
Jon Salz0405ab52012-03-16 15:26:52 +0800131GLOBAL_HOT_KEY_EVENTS = {
132 'r': Event.Type.RESTART_TESTS,
133 'a': Event.Type.AUTO_RUN,
134 'f': Event.Type.RE_RUN_FAILED,
Hung-Te Lin96632362012-03-20 21:14:18 +0800135 'z': Event.Type.REVIEW,
Jon Salz0405ab52012-03-16 15:26:52 +0800136 }
137try:
138 # Works only if X is available.
139 GLOBAL_HOT_KEY_MASK = X.ControlMask | X.Mod1Mask
140except:
141 pass
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800142
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800143# ---------------------------------------------------------------------------
144# Client Library
145
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800146
Hung-Te Lin4b0b61f2012-03-05 14:20:06 +0800147# TODO(hungte) Replace gtk_lock by gtk.gdk.lock when it's availble (need pygtk
148# 2.2x, and we're now pinned by 2.1x)
149class _GtkLock(object):
150 __enter__ = gtk.gdk.threads_enter
151 def __exit__(*ignored):
152 gtk.gdk.threads_leave()
153
154
155gtk_lock = _GtkLock()
156
157
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800158def make_label(message, font=LABEL_FONT, fg=LIGHT_GREEN,
159 size=None, alignment=None):
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800160 """Returns a label widget.
161
162 A wrapper for gtk.Label. The unit of size is pixels under resolution
163 _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
164
165 @param message: A string to be displayed.
166 @param font: Font descriptor for the label.
167 @param fg: Foreground color.
168 @param size: Minimum size for this label.
169 @param alignment: Alignment setting.
170 @return: A label widget.
171 """
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800172 l = gtk.Label(message)
173 l.modify_font(font)
174 l.modify_fg(gtk.STATE_NORMAL, fg)
175 if size:
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800176 # Convert size according to the current resolution.
177 l.set_size_request(*convert_pixels(size))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800178 if alignment:
179 l.set_alignment(*alignment)
180 return l
181
182
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800183def make_status_row(init_prompt,
184 init_status,
185 label_size=_LABEL_STATUS_ROW_SIZE):
186 """Returns a widget that live updates prompt and status in a row.
187
188 Args:
189 init_prompt: The prompt label text.
190 init_status: The status label text.
191 label_size: The desired size of the prompt label and the status label.
192
193 Returns:
194 1) A dict whose content is linked by the widget.
195 2) A widget to render dict content in "prompt: status" format.
196 """
197 display_dict = {}
198 display_dict['prompt'] = init_prompt
199 display_dict['status'] = init_status
200
201 def prompt_label_expose(widget, event):
202 prompt = display_dict['prompt']
203 widget.set_text(prompt)
204
205 def status_label_expose(widget, event):
206 status = display_dict['status']
207 widget.set_text(status)
208 widget.modify_fg(gtk.STATE_NORMAL, LABEL_COLORS[status])
209
210 prompt_label = make_label(
211 init_prompt, size=label_size,
212 alignment=(0, 0.5))
213 delimiter_label = make_label(':', alignment=(0, 0.5))
214 status_label = make_label(
215 init_status, size=label_size,
216 alignment=(0, 0.5), fg=LABEL_COLORS[init_status])
217
218 widget = gtk.HBox()
219 widget.pack_end(status_label, False, False)
220 widget.pack_end(delimiter_label, False, False)
221 widget.pack_end(prompt_label, False, False)
222
223 status_label.connect('expose_event', status_label_expose)
224 prompt_label.connect('expose_event', prompt_label_expose)
225 return display_dict, widget
226
227
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800228def convert_pixels(size):
229 """Converts a pair in pixel that is suitable for current resolution.
230
231 GTK takes pixels as its unit in many function calls. To maintain the
232 consistency of the UI in different resolution, a conversion is required.
233 Take current resolution and (_UI_SCREEN_WIDTH, _UI_SCREEN_HEIGHT) as
234 the original resolution, this function returns a pair of width and height
235 that is converted for current resolution.
236
237 Because pixels in negative usually indicates unspecified, no conversion
238 will be done for negative pixels.
239
240 In addition, the aspect ratio is not maintained in this function.
241
242 Usage Example:
243 width,_ = convert_pixels((20,-1))
244
245 @param size: A pair of pixels that designed under original resolution.
246 @return: A pair of pixels of (width, height) format.
247 Pixels returned are always integer.
248 """
249 return (int(float(size[0]) / _UI_SCREEN_WIDTH * gtk.gdk.screen_width()
250 if (size[0] > 0) else size[0]),
251 int(float(size[1]) / _UI_SCREEN_HEIGHT * gtk.gdk.screen_height()
252 if (size[1] > 0) else size[1]))
253
254
255def make_hsep(height=1):
256 """Returns a widget acts as a horizontal separation line.
257
258 The unit is pixels under resolution _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
259 """
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800260 frame = gtk.EventBox()
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800261 # Convert height according to the current resolution.
262 frame.set_size_request(*convert_pixels((-1, height)))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800263 frame.modify_bg(gtk.STATE_NORMAL, SEP_COLOR)
264 return frame
265
266
267def make_vsep(width=1):
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800268 """Returns a widget acts as a vertical separation line.
269
270 The unit is pixels under resolution _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
271 """
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800272 frame = gtk.EventBox()
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800273 # Convert width according to the current resolution.
274 frame.set_size_request(*convert_pixels((width, -1)))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800275 frame.modify_bg(gtk.STATE_NORMAL, SEP_COLOR)
276 return frame
277
278
Hung-Te Linaf8ec542012-03-14 20:01:40 +0800279def make_countdown_widget(prompt=None, value=None, fg=LIGHT_GREEN):
280 if prompt is None:
281 prompt = 'time remaining / 剩餘時間: '
282 if value is None:
283 value = '%s' % FAIL_TIMEOUT
284 title = make_label(prompt, fg=fg, alignment=(1, 0.5))
285 countdown = make_label(value, fg=fg, alignment=(0, 0.5))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800286 hbox = gtk.HBox()
287 hbox.pack_start(title)
288 hbox.pack_start(countdown)
289 eb = gtk.EventBox()
290 eb.modify_bg(gtk.STATE_NORMAL, BLACK)
291 eb.add(hbox)
292 return eb, countdown
293
294
295def hide_cursor(gdk_window):
296 pixmap = gtk.gdk.Pixmap(None, 1, 1, 1)
297 color = gtk.gdk.Color()
298 cursor = gtk.gdk.Cursor(pixmap, pixmap, color, color, 0, 0)
299 gdk_window.set_cursor(cursor)
300
301
302def calc_scale(wanted_x, wanted_y):
303 (widget_size_x, widget_size_y) = factory.get_shared_data('test_widget_size')
304 scale_x = (0.9 * widget_size_x) / wanted_x
305 scale_y = (0.9 * widget_size_y) / wanted_y
306 scale = scale_y if scale_y < scale_x else scale_x
307 scale = 1 if scale > 1 else scale
308 factory.log('scale: %s' % scale)
309 return scale
310
311
312def trim(text, length):
313 if len(text) > length:
314 text = text[:length-3] + '...'
315 return text
316
317
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800318class InputError(ValueError):
319 """Execption for input window callbacks to change status text message."""
320 pass
321
322
Hung-Te Linbf545582012-02-15 17:08:07 +0800323def make_input_window(prompt=None,
324 init_value=None,
325 msg_invalid=None,
326 font=None,
327 on_validate=None,
328 on_keypress=None,
329 on_complete=None):
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800330 """Creates a widget to prompt user for a valid string.
Hung-Te Linbf545582012-02-15 17:08:07 +0800331
332 @param prompt: A string to be displayed. None for default message.
333 @param init_value: Initial value to be set.
334 @param msg_invalid: Status string to display when input is invalid. None for
335 default message.
336 @param font: Font specification (string or pango.FontDescription) for label
337 and entry. None for default large font.
338 @param on_validate: A callback function to validate if the input from user
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800339 is valid. None for allowing any non-empty input. Any ValueError or
340 ui.InputError raised during execution in on_validate will be displayed
341 in bottom status.
Hung-Te Linbf545582012-02-15 17:08:07 +0800342 @param on_keypress: A callback function when each keystroke is hit.
343 @param on_complete: A callback function when a valid string is passed.
344 None to stop (gtk.main_quit).
Hung-Te Lin7130d9e2012-03-16 15:41:18 +0800345 @return: A widget with prompt, input entry, and status label. To access
346 these elements, use attribute 'prompt', 'entry', and 'label'.
Hung-Te Linbf545582012-02-15 17:08:07 +0800347 """
348 DEFAULT_MSG_INVALID = "Invalid input / 輸入不正確"
349 DEFAULT_PROMPT = "Enter Data / 輸入資料:"
350
351 def enter_callback(entry):
352 text = entry.get_text()
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800353 try:
354 if (on_validate and (not on_validate(text))) or (not text.strip()):
355 raise ValueError(msg_invalid)
Hung-Te Linbf545582012-02-15 17:08:07 +0800356 on_complete(text) if on_complete else gtk.main_quit()
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800357 except ValueError as e:
358 gtk.gdk.beep()
359 status_label.set_text('ERROR: %s' % e.message)
Hung-Te Linbf545582012-02-15 17:08:07 +0800360 return True
361
362 def key_press_callback(entry, key):
363 status_label.set_text('')
364 if on_keypress:
365 return on_keypress(entry, key)
366 return False
367
368 # Populate default parameters
369 if msg_invalid is None:
370 msg_invalid = DEFAULT_MSG_INVALID
371
372 if prompt is None:
373 prompt = DEFAULT_PROMPT
374
375 if font is None:
376 font = LABEL_LARGE_FONT
377 elif not isinstance(font, pango.FontDescription):
378 font = pango.FontDescription(font)
379
380 widget = gtk.VBox()
381 label = make_label(prompt, font=font)
382 status_label = make_label('', font=font)
383 entry = gtk.Entry()
384 entry.modify_font(font)
385 entry.connect("activate", enter_callback)
386 entry.connect("key_press_event", key_press_callback)
387 if init_value:
388 entry.set_text(init_value)
389 widget.modify_bg(gtk.STATE_NORMAL, BLACK)
390 status_label.modify_fg(gtk.STATE_NORMAL, RED)
391 widget.add(label)
392 widget.pack_start(entry)
393 widget.pack_start(status_label)
Chun-ta Lin2e7f44c2012-03-03 07:29:36 +0800394
Hung-Te Lin7130d9e2012-03-16 15:41:18 +0800395 widget.entry = entry
396 widget.status = status_label
397 widget.prompt = label
398
399 # TODO(itspeter) Replace deprecated get_entry by widget.entry.
Chun-ta Lin2e7f44c2012-03-03 07:29:36 +0800400 # Method for getting the entry.
401 widget.get_entry = lambda : entry
Hung-Te Linbf545582012-02-15 17:08:07 +0800402 return widget
403
404
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800405def make_summary_box(tests, state_map, rows=15):
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800406 '''Creates a widget display status of a set of test.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800407
408 @param tests: A list of FactoryTest nodes whose status (and children's
409 status) should be displayed.
410 @param state_map: The state map as provide by the state instance.
411 @param rows: The number of rows to display.
412 @return: A tuple (widget, label_map), where widget is the widget, and
413 label_map is a map from each test to the corresponding label.
414 '''
415 LABEL_EN_SIZE = (170, 35)
416 LABEL_EN_SIZE_2 = (450, 25)
417 LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
418
419 all_tests = sum([list(t.walk(in_order=True)) for t in tests], [])
420 columns = len(all_tests) / rows + (len(all_tests) % rows != 0)
421
422 info_box = gtk.HBox()
423 info_box.set_spacing(20)
424 for status in (TestState.ACTIVE, TestState.PASSED,
425 TestState.FAILED, TestState.UNTESTED):
426 label = make_label(status,
427 size=LABEL_EN_SIZE,
428 font=LABEL_EN_FONT,
429 alignment=(0.5, 0.5),
430 fg=LABEL_COLORS[status])
431 info_box.pack_start(label, False, False)
432
433 vbox = gtk.VBox()
434 vbox.set_spacing(20)
435 vbox.pack_start(info_box, False, False)
436
437 label_map = {}
438
439 if all_tests:
440 status_table = gtk.Table(rows, columns, True)
441 for (j, i), t in izip(product(xrange(columns), xrange(rows)),
442 all_tests):
443 msg_en = ' ' * (t.depth() - 1) + t.label_en
444 msg_en = trim(msg_en, 12)
445 if t.label_zh:
446 msg = '{0:<12} ({1})'.format(msg_en, t.label_zh)
447 else:
448 msg = msg_en
449 status = state_map[t].status
450 status_label = make_label(msg,
451 size=LABEL_EN_SIZE_2,
452 font=LABEL_EN_FONT,
453 alignment=(0.0, 0.5),
454 fg=LABEL_COLORS[status])
455 label_map[t] = status_label
456 status_table.attach(status_label, j, j+1, i, i+1)
457 vbox.pack_start(status_table, False, False)
458
459 return vbox, label_map
460
461
462def run_test_widget(dummy_job, test_widget,
463 invisible_cursor=True,
464 window_registration_callback=None,
465 cleanup_callback=None):
466 test_widget_size = factory.get_shared_data('test_widget_size')
467
468 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
469 window.modify_bg(gtk.STATE_NORMAL, BLACK)
470 window.set_size_request(*test_widget_size)
471
472 def show_window():
473 window.show()
474 window.window.raise_() # pylint: disable=E1101
475 gtk.gdk.pointer_grab(window.window, confine_to=window.window)
476 if invisible_cursor:
477 hide_cursor(window.window)
478
479 test_path = factory.get_current_test_path()
480
481 def handle_event(event):
482 if (event.type == Event.Type.STATE_CHANGE and
483 test_path and event.path == test_path and
484 event.state.visible):
485 show_window()
486
487 event_client = EventClient(
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800488 callback=handle_event, event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800489
490 align = gtk.Alignment(xalign=0.5, yalign=0.5)
491 align.add(test_widget)
492
493 window.add(align)
494 for c in window.get_children():
495 # Show all children, but not the window itself yet.
496 c.show_all()
497
498 if window_registration_callback is not None:
499 window_registration_callback(window)
500
501 # Show the window if it is the visible test, or if the test_path is not
502 # available (e.g., run directly from the command line).
503 if (not test_path) or (
504 TestState.from_dict_or_object(
505 factory.get_state_instance().get_test_state(test_path)).visible):
506 show_window()
507 else:
508 window.hide()
509
Hung-Te Linde45e9c2012-03-19 13:02:06 +0800510 # When gtk.main() is running, it ignores all uncaught exceptions, which is
511 # not preferred by most of our factory tests. To prevent writing special
512 # function raising errors, we hook top level exception handler to always
513 # leave GTK main and raise exception again.
514
515 def exception_hook(exc_type, value, traceback):
516 # Prevent re-entrant.
517 sys.excepthook = old_excepthook
518 session['exception'] = (exc_type, value, traceback)
519 gobject.idle_add(gtk.main_quit)
520 return old_excepthook(exc_type, value, traceback)
521
522 session = {}
523 old_excepthook = sys.excepthook
524 sys.excepthook = exception_hook
525
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800526 gtk.main()
527
528 gtk.gdk.pointer_ungrab()
529
530 if cleanup_callback is not None:
531 cleanup_callback()
532
533 del event_client
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800534
Hung-Te Linde45e9c2012-03-19 13:02:06 +0800535 sys.excepthook = old_excepthook
536 exc_info = session.get('exception')
537 if exc_info is not None:
538 logging.error(exc_info[0], exc_info=exc_info)
539 raise error.TestError(exc_info[1])
540
541
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800542
543# ---------------------------------------------------------------------------
544# Server Implementation
545
546
547class Console(object):
548 '''Display a progress log. Implemented by launching an borderless
549 xterm at a strategic location, and running tail against the log.'''
550
551 def __init__(self, allocation):
Chun-Ta Lin734e96a2012-03-16 20:05:33 +0800552 # Specify how many lines and characters per line are displayed.
553 XTERM_DISPLAY_LINES = 13
554 XTERM_DISPLAY_CHARS = 120
555 # Extra space reserved for pixels between lines.
556 XTERM_RESERVED_LINES = 3
557
558 xterm_coords = '%dx%d+%d+%d' % (XTERM_DISPLAY_CHARS,
559 XTERM_DISPLAY_LINES,
560 allocation.x,
561 allocation.y)
562 xterm_reserved_height = gtk.gdk.screen_height() - allocation.y
563 font_size = int(float(xterm_reserved_height) / (XTERM_DISPLAY_LINES +
564 XTERM_RESERVED_LINES))
565 logging.info('xterm_reserved_height = %d' % xterm_reserved_height)
566 logging.info('font_size = %d' % font_size)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800567 logging.info('xterm_coords = %s', xterm_coords)
568 xterm_opts = ('-bg black -fg lightgray -bw 0 -g %s' % xterm_coords)
Chun-Ta Lin734e96a2012-03-16 20:05:33 +0800569 xterm_cmd = (
570 ['urxvt'] + xterm_opts.split() +
571 ['-fn', 'xft:DejaVu Sans Mono:pixelsize=%s' % font_size] +
572 ['-e', 'bash'] +
573 ['-c', 'tail -f "%s"' % factory.CONSOLE_LOG_PATH])
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800574 logging.info('xterm_cmd = %s', xterm_cmd)
575 self._proc = subprocess.Popen(xterm_cmd)
576
577 def __del__(self):
578 logging.info('console_proc __del__')
579 self._proc.kill()
580
581
582class TestLabelBox(gtk.EventBox): # pylint: disable=R0904
583
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800584 def __init__(self, test):
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800585 gtk.EventBox.__init__(self)
586 self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[TestState.UNTESTED])
Hung-Te Line94e0a02012-03-19 18:20:35 +0800587 self._is_group = test.is_group()
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800588 depth = len(test.get_ancestor_groups())
Hung-Te Line94e0a02012-03-19 18:20:35 +0800589 self._label_text = ' %s%s%s' % (
590 ' ' * depth,
591 SYMBOL_RIGHT_ARROW if self._is_group else ' ',
592 test.label_en)
593 if self._is_group:
594 self._label_text_collapsed = ' %s%s%s' % (
595 ' ' * depth,
596 SYMBOL_DOWN_ARROW if self._is_group else '',
597 test.label_en)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800598 self._label_en = make_label(
Hung-Te Line94e0a02012-03-19 18:20:35 +0800599 self._label_text, size=_LABEL_EN_SIZE,
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800600 font=_LABEL_EN_FONT, alignment=(0, 0.5),
601 fg=_LABEL_UNTESTED_FG)
602 self._label_zh = make_label(
603 test.label_zh, size=_LABEL_ZH_SIZE,
604 font=_LABEL_ZH_FONT, alignment=(0.5, 0.5),
605 fg=_LABEL_UNTESTED_FG)
606 self._label_t = make_label(
607 '', size=_LABEL_T_SIZE, font=_LABEL_T_FONT,
608 alignment=(0.5, 0.5), fg=BLACK)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800609 hbox = gtk.HBox()
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800610 hbox.pack_start(self._label_en, False, False)
611 hbox.pack_start(self._label_zh, False, False)
612 hbox.pack_start(self._label_t, False, False)
613 vbox = gtk.VBox()
614 vbox.pack_start(hbox, False, False)
615 vbox.pack_start(make_hsep(), False, False)
616 self.add(vbox)
617 self._status = None
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800618
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800619 def set_shortcut(self, shortcut):
620 if shortcut is None:
621 return
Hung-Te Lin99f64ef2012-03-22 11:05:45 +0800622 self._label_t.set_text('C-%s' % shortcut.upper())
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800623 attrs = self._label_en.get_attributes() or pango.AttrList()
624 attrs.filter(lambda attr: attr.type == pango.ATTR_UNDERLINE)
625 index_hotkey = self._label_en.get_text().upper().find(shortcut.upper())
626 if index_hotkey != -1:
627 attrs.insert(pango.AttrUnderline(
628 pango.UNDERLINE_LOW, index_hotkey, index_hotkey + 1))
629 attrs.insert(pango.AttrWeight(
630 pango.WEIGHT_BOLD, index_hotkey, index_hotkey + 1))
631 self._label_en.set_attributes(attrs)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800632 self.queue_draw()
633
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800634 def update(self, status):
635 if self._status == status:
636 return
637 self._status = status
638 label_fg = (_LABEL_UNTESTED_FG if status == TestState.UNTESTED
639 else BLACK)
Hung-Te Line94e0a02012-03-19 18:20:35 +0800640 if self._is_group:
641 self._label_en.set_text(
642 self._label_text_collapsed if status == TestState.ACTIVE
643 else self._label_text)
644
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800645 for label in [self._label_en, self._label_zh, self._label_t]:
646 label.modify_fg(gtk.STATE_NORMAL, label_fg)
647 self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[status])
648 self.queue_draw()
649
650
Hung-Te Lin96632362012-03-20 21:14:18 +0800651class ReviewInformation(object):
652
653 LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
654 TAB_BORDER = 20
655
656 def __init__(self, test_list):
657 self.test_list = test_list
658
659 def make_error_tab(self, test, state):
660 msg = '%s (%s)\n%s' % (test.label_en, test.label_zh,
661 str(state.error_msg))
662 label = make_label(msg, font=self.LABEL_EN_FONT, alignment=(0.0, 0.0))
663 label.set_line_wrap(True)
664 frame = gtk.Frame()
665 frame.add(label)
666 return frame
667
668 def make_widget(self):
669 bg_color = gtk.gdk.Color(0x1000, 0x1000, 0x1000)
670 self.notebook = gtk.Notebook()
671 self.notebook.modify_bg(gtk.STATE_NORMAL, bg_color)
672
673 test_list = self.test_list
674 state_map = test_list.get_state_map()
675 tab, _ = make_summary_box([test_list], state_map)
676 tab.set_border_width(self.TAB_BORDER)
677 self.notebook.append_page(tab, make_label('Summary'))
678
679 for i, t in izip(
680 count(1),
681 [t for t in test_list.walk()
682 if state_map[t].status == factory.TestState.FAILED
683 and t.is_leaf()]):
684 tab = self.make_error_tab(t, state_map[t])
685 tab.set_border_width(self.TAB_BORDER)
686 self.notebook.append_page(tab, make_label('#%02d' % i))
687
688 prompt = 'Review: Test Status Information'
689 if self.notebook.get_n_pages() > 1:
690 prompt += '\nPress left/right to change tabs'
691
692 control_label = make_label(prompt, font=self.LABEL_EN_FONT,
693 alignment=(0.5, 0.5))
694 vbox = gtk.VBox()
695 vbox.set_spacing(self.TAB_BORDER)
696 vbox.pack_start(control_label, False, False)
697 vbox.pack_start(self.notebook, False, False)
698 vbox.show_all()
699 vbox.grab_focus = self.notebook.grab_focus
700 return vbox
701
702
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800703class TestDirectory(gtk.VBox):
704 '''Widget containing a list of tests, colored by test status.
705
706 This is the widget corresponding to the RHS test panel.
707
708 Attributes:
709 _label_map: Dict of test path to TestLabelBox objects. Should
710 contain an entry for each test that has been visible at some
711 time.
712 _visible_status: List of (test, status) pairs reflecting the
713 last refresh of the set of visible tests. This is used to
714 rememeber what tests were active, to allow implementation of
715 visual refresh only when new active tests appear.
716 _shortcut_map: Dict of keyboard shortcut key to test path.
717 Tracks the current set of keyboard shortcut mappings for the
718 visible set of tests. This will change when the visible
719 test set changes.
720 '''
721
Tammo Spalinkddae4df2012-03-22 11:50:00 +0800722 def __init__(self, test_list):
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800723 gtk.VBox.__init__(self)
724 self.set_spacing(0)
725 self._label_map = {}
726 self._visible_status = []
727 self._shortcut_map = {}
Tammo Spalinkddae4df2012-03-22 11:50:00 +0800728 self._hard_shortcuts = set(
729 test.kbd_shortcut for test in test_list.walk()
730 if test.kbd_shortcut is not None)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800731
732 def _get_test_label(self, test):
733 if test.path in self._label_map:
734 return self._label_map[test.path]
735 label_box = TestLabelBox(test)
736 self._label_map[test.path] = label_box
737 return label_box
738
739 def _remove_shortcut(self, path):
740 reverse_map = dict((v, k) for k, v in self._shortcut_map.items())
741 if path not in reverse_map:
742 logging.error('Removal of non-present shortcut for %s' % path)
743 return
744 shortcut = reverse_map[path]
745 del self._shortcut_map[shortcut]
746
747 def _add_shortcut(self, test):
748 shortcut = test.kbd_shortcut
749 if shortcut in self._shortcut_map:
750 logging.error('Shortcut %s already in use by %s; cannot apply to %s'
751 % (shortcut, self._shortcut_map[shortcut], test.path))
752 shortcut = None
753 if shortcut is None:
754 # Find a suitable shortcut. For groups, use numbers. For
755 # regular tests, use alpha (letters).
Jon Salz0405ab52012-03-16 15:26:52 +0800756 if test.is_group():
757 gen = (x for x in string.digits if x not in self._shortcut_map)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800758 else:
Jon Salz0405ab52012-03-16 15:26:52 +0800759 gen = (x for x in test.label_en.lower() + string.lowercase
Tammo Spalinkddae4df2012-03-22 11:50:00 +0800760 if x.isalnum() and x not in self._shortcut_map
761 and x not in self._hard_shortcuts)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800762 shortcut = next(gen, None)
763 if shortcut is None:
764 logging.error('Unable to find shortcut for %s' % test.path)
765 return
766 self._shortcut_map[shortcut] = test.path
767 return shortcut
768
769 def handle_xevent(self, dummy_src, dummy_cond,
770 xhandle, keycode_map, event_client):
771 for dummy_i in range(0, xhandle.pending_events()):
772 xevent = xhandle.next_event()
773 if xevent.type != X.KeyPress:
774 continue
775 keycode = xevent.detail
776 if keycode not in keycode_map:
777 logging.warning('Ignoring unknown keycode %r' % keycode)
778 continue
779 shortcut = keycode_map[keycode]
Jon Salz0405ab52012-03-16 15:26:52 +0800780
Hung-Te Lin96632362012-03-20 21:14:18 +0800781 if (xevent.state & GLOBAL_HOT_KEY_MASK == GLOBAL_HOT_KEY_MASK):
Jon Salz0405ab52012-03-16 15:26:52 +0800782 event_type = GLOBAL_HOT_KEY_EVENTS.get(shortcut)
783 if event_type:
784 event_client.post_event(Event(event_type))
785 else:
Jon Salz968e90b2012-03-18 16:12:43 +0800786 logging.warning('Unbound global hot key %s', key)
Jon Salz0405ab52012-03-16 15:26:52 +0800787 else:
788 if shortcut not in self._shortcut_map:
789 logging.warning('Ignoring unbound shortcut %r' % shortcut)
790 continue
791 test_path = self._shortcut_map[shortcut]
792 event_client.post_event(Event(Event.Type.SWITCH_TEST,
793 path=test_path))
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800794 return True
795
796 def update(self, new_test_status):
797 '''Refresh the RHS test list to show current status and active groups.
798
799 Refresh the set of visible tests only when new active tests
800 arise. This avoids visual volatility when switching between
801 tests (intervals where no test is active). Also refresh at
802 initial startup.
803
804 Args:
805 new_test_status: A list of (test, status) tuples. The tests
806 order should match how they should be displayed in the
807 directory (rhs panel).
808 '''
809 old_active = set(t for t, s in self._visible_status
810 if s == TestState.ACTIVE)
811 new_active = set(t for t, s in new_test_status
812 if s == TestState.ACTIVE)
813 new_visible = set(t for t, s in new_test_status)
814 old_visible = set(t for t, s in self._visible_status)
815
816 if old_active and not new_active - old_active:
817 # No new active tests, so do not change the displayed test
818 # set, only update the displayed status for currently
819 # visible tests. Not updating _visible_status allows us
820 # to remember the last set of active tests.
821 for test, _ in self._visible_status:
822 status = test.get_state().status
823 self._label_map[test.path].update(status)
824 return
825
826 self._visible_status = new_test_status
827
828 new_test_map = dict((t.path, t) for t, s in new_test_status)
829
830 for test in old_visible - new_visible:
831 label_box = self._label_map[test.path]
832 logging.debug('removing %s test label' % test.path)
833 self.remove(label_box)
834 self._remove_shortcut(test.path)
835
836 new_tests = new_visible - old_visible
837
838 for position, (test, status) in enumerate(new_test_status):
839 label_box = self._get_test_label(test)
840 if test in new_tests:
841 shortcut = self._add_shortcut(test)
842 label_box = self._get_test_label(test)
843 label_box.set_shortcut(shortcut)
844 logging.debug('adding %s test label (sortcut %r, pos %d)' %
845 (test.path, shortcut, position))
846 self.pack_start(label_box, False, False)
847 self.reorder_child(label_box, position)
848 label_box.update(status)
849
850 self.show_all()
851
852
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800853
854class UiState(object):
855
Hung-Te Lin96632362012-03-20 21:14:18 +0800856 WIDGET_NONE = 0
857 WIDGET_IDLE = 1
858 WIDGET_SUMMARY = 2
859 WIDGET_REVIEW = 3
860
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800861 def __init__(self, test_widget_box, test_directory_widget, test_list):
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800862 self._test_widget_box = test_widget_box
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800863 self._test_directory_widget = test_directory_widget
864 self._test_list = test_list
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800865 self._transition_count = 0
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800866 self._active_test_label_map = None
Hung-Te Lin96632362012-03-20 21:14:18 +0800867 self._active_widget = self.WIDGET_NONE
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800868 self.update_test_state()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800869
Hung-Te Lin96632362012-03-20 21:14:18 +0800870 def show_idle_widget(self):
871 self.remove_state_widget()
872 self._test_widget_box.set(0.5, 0.5, 0.0, 0.0)
873 self._test_widget_box.set_padding(0, 0, 0, 0)
874 label = make_label(MESSAGE_NO_ACTIVE_TESTS,
875 font=_OTHER_LABEL_FONT,
876 alignment=(0.5, 0.5))
877 self._test_widget_box.add(label)
878 self._test_widget_box.show_all()
879 self._active_widget = self.WIDGET_IDLE
880
881 def show_summary_widget(self):
882 self.remove_state_widget()
883 state_map = self._test_list.get_state_map()
884 self._test_widget_box.set(0.5, 0.0, 0.0, 0.0)
885 self._test_widget_box.set_padding(40, 0, 0, 0)
886 vbox, self._active_test_label_map = make_summary_box(
887 [t for t in self._test_list.subtests
888 if state_map[t].status == TestState.ACTIVE],
889 state_map)
890 self._test_widget_box.add(vbox)
891 self._test_widget_box.show_all()
892 self._active_widget = self.WIDGET_SUMMARY
893
894 def show_review_widget(self):
895 self.remove_state_widget()
896 self._review_request = False
897 self._test_widget_box.set(0.5, 0.5, 0.0, 0.0)
898 self._test_widget_box.set_padding(0, 0, 0, 0)
899 widget = ReviewInformation(self._test_list).make_widget()
900 self._test_widget_box.add(widget)
901 self._test_widget_box.show_all()
902 widget.grab_focus()
903 self._active_widget = self.WIDGET_REVIEW
904
905 def remove_state_widget(self):
906 for child in self._test_widget_box.get_children():
907 child.hide()
908 self._test_widget_box.remove(child)
909 self._active_test_label_map = None
910 self._active_widget = self.WIDGET_NONE
911
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800912 def update_test_state(self):
913 state_map = self._test_list.get_state_map()
914 active_tests = set(
915 t for t in self._test_list.walk()
916 if t.is_leaf() and state_map[t].status == TestState.ACTIVE)
917 active_groups = set(g for t in active_tests
918 for g in t.get_ancestor_groups())
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800919
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800920 def filter_visible_test_state(tests):
921 '''List currently visible tests and their status.
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800922
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800923 Visible means currently displayed in the RHS panel.
924 Visiblity is implied by being a top level test or having
925 membership in a group with at least one active test.
926
927 Returns:
928 A list of (test, status) tuples for all visible tests,
929 in the order they should be displayed.
930 '''
931 results = []
932 for test in tests:
Jon Salz0405ab52012-03-16 15:26:52 +0800933 if test.is_group():
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800934 results.append((test, TestState.UNTESTED))
935 if test not in active_groups:
936 continue
937 results += filter_visible_test_state(test.subtests)
938 else:
939 results.append((test, state_map[test].status))
940 return results
941
942 visible_test_state = filter_visible_test_state(self._test_list.subtests)
943 self._test_directory_widget.update(visible_test_state)
944
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800945 if not active_tests:
Hung-Te Lin96632362012-03-20 21:14:18 +0800946 # Display the idle or review information screen.
947 def waiting_for_transition():
948 return (self._active_widget not in
949 [self.WIDGET_REVIEW, self.WIDGET_IDLE])
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800950
Hung-Te Lin96632362012-03-20 21:14:18 +0800951 # For smooth transition between tests, idle widget if activated only
952 # after _NO_ACTIVE_TEST_DELAY_MS without state change.
953 def idle_transition_check(cookie):
954 if (waiting_for_transition() and
955 cookie == self._transition_count):
956 self._transition_count += 1
957 self.show_idle_widget()
958 return False
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800959
Hung-Te Lin96632362012-03-20 21:14:18 +0800960 if waiting_for_transition():
961 gobject.timeout_add(_NO_ACTIVE_TEST_DELAY_MS,
962 idle_transition_check,
963 self._transition_count)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800964 return
965
966 self._transition_count += 1
967
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800968 if any(t.has_ui for t in active_tests):
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800969 # Remove the widget (if any) since there is an active test
970 # with a UI.
Hung-Te Lin96632362012-03-20 21:14:18 +0800971 self.remove_state_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800972 return
973
974 if (self._active_test_label_map is not None and
975 all(t in self._active_test_label_map for t in active_tests)):
976 # All active tests are already present in the summary, so just
977 # update their states.
978 for test, label in self._active_test_label_map.iteritems():
979 label.modify_fg(
980 gtk.STATE_NORMAL,
981 LABEL_COLORS[state_map[test].status])
982 return
983
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800984 # No active UI; draw summary of current test states
Hung-Te Lin96632362012-03-20 21:14:18 +0800985 self.show_summary_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800986
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800987
988def grab_shortcut_keys(disp, event_handler, event_client):
989 # We want to receive KeyPress events
990 root = disp.screen().root
991 root.change_attributes(event_mask = X.KeyPressMask)
992 shortcut_set = set(string.lowercase + string.digits)
993 keycode_map = {}
994 for mod, shortcut in ([(X.ControlMask, k) for k in shortcut_set] +
Jon Salz0405ab52012-03-16 15:26:52 +0800995 [(GLOBAL_HOT_KEY_MASK, k)
996 for k in GLOBAL_HOT_KEY_EVENTS] +
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800997 [(X.Mod1Mask, 'Tab')]): # Mod1 = Alt
998 keysym = gtk.gdk.keyval_from_name(shortcut)
999 keycode = disp.keysym_to_keycode(keysym)
1000 keycode_map[keycode] = shortcut
1001 root.grab_key(keycode, mod, 1, X.GrabModeAsync, X.GrabModeAsync)
1002 # This flushes the XGrabKey calls to the server.
1003 for dummy_x in range(0, root.display.pending_events()):
1004 root.display.next_event()
1005 gobject.io_add_watch(root.display, gobject.IO_IN, event_handler,
1006 root.display, keycode_map, event_client)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001007
1008
1009def main(test_list_path):
1010 '''Starts the main UI.
1011
1012 This is launched by the autotest/cros/factory/client.
1013 When operators press keyboard shortcuts, the shortcut
1014 value is sent as an event to the control program.'''
1015
1016 test_list = None
1017 ui_state = None
1018 event_client = None
1019
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001020 def handle_key_release_event(_, event):
1021 logging.info('base ui key event (%s)', event.keyval)
1022 return True
1023
1024 def handle_event(event):
1025 if event.type == Event.Type.STATE_CHANGE:
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001026 ui_state.update_test_state()
Hung-Te Lin96632362012-03-20 21:14:18 +08001027 elif event.type == Event.Type.REVIEW:
1028 logging.info("Operator activates review information screen")
1029 ui_state.show_review_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001030
1031 test_list = factory.read_test_list(test_list_path)
1032
1033 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
1034 window.connect('destroy', lambda _: gtk.main_quit())
1035 window.modify_bg(gtk.STATE_NORMAL, BLACK)
1036
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001037 disp = Display()
1038
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001039 event_client = EventClient(
1040 callback=handle_event,
1041 event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
1042
1043 screen = window.get_screen()
1044 if (screen is None):
1045 logging.info('ERROR: communication with the X server is not working, ' +
1046 'could not find a working screen. UI exiting.')
1047 sys.exit(1)
1048
1049 screen_size_str = os.environ.get('CROS_SCREEN_SIZE')
1050 if screen_size_str:
1051 match = re.match(r'^(\d+)x(\d+)$', screen_size_str)
1052 assert match, 'CROS_SCREEN_SIZE should be {width}x{height}'
1053 screen_size = (int(match.group(1)), int(match.group(2)))
1054 else:
1055 screen_size = (screen.get_width(), screen.get_height())
1056 window.set_size_request(*screen_size)
1057
Tammo Spalinkddae4df2012-03-22 11:50:00 +08001058 test_directory = TestDirectory(test_list)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001059
1060 rhs_box = gtk.EventBox()
1061 rhs_box.modify_bg(gtk.STATE_NORMAL, _LABEL_TROUGH_COLOR)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001062 rhs_box.add(test_directory)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001063
1064 console_box = gtk.EventBox()
Chun-Ta Lin734e96a2012-03-16 20:05:33 +08001065 console_box.set_size_request(*convert_pixels((-1, 180)))
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001066 console_box.modify_bg(gtk.STATE_NORMAL, BLACK)
1067
1068 test_widget_box = gtk.Alignment()
1069 test_widget_box.set_size_request(-1, -1)
1070
1071 lhs_box = gtk.VBox()
1072 lhs_box.pack_end(console_box, False, False)
1073 lhs_box.pack_start(test_widget_box)
1074 lhs_box.pack_start(make_hsep(3), False, False)
1075
1076 base_box = gtk.HBox()
1077 base_box.pack_end(rhs_box, False, False)
1078 base_box.pack_end(make_vsep(3), False, False)
1079 base_box.pack_start(lhs_box)
1080
1081 window.connect('key-release-event', handle_key_release_event)
1082 window.add_events(gtk.gdk.KEY_RELEASE_MASK)
1083
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001084 ui_state = UiState(test_widget_box, test_directory, test_list)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001085
1086 window.add(base_box)
1087 window.show_all()
1088
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001089 grab_shortcut_keys(disp, test_directory.handle_xevent, event_client)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001090
1091 hide_cursor(window.window)
1092
1093 test_widget_allocation = test_widget_box.get_allocation()
1094 test_widget_size = (test_widget_allocation.width,
1095 test_widget_allocation.height)
1096 factory.set_shared_data('test_widget_size', test_widget_size)
1097
Jon Salz758e6cc2012-04-03 15:47:07 +08001098 if not factory.in_chroot():
1099 dummy_console = Console(console_box.get_allocation())
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001100
1101 event_client.post_event(Event(Event.Type.UI_READY))
1102
1103 logging.info('cros/factory/ui setup done, starting gtk.main()...')
1104 gtk.main()
1105 logging.info('cros/factory/ui gtk.main() finished, exiting.')
1106
1107
1108if __name__ == '__main__':
Jon Salz14bcbb02012-03-17 15:11:50 +08001109 parser = OptionParser(usage='usage: %prog [options] TEST-LIST-PATH')
1110 parser.add_option('-v', '--verbose', dest='verbose',
1111 action='store_true',
1112 help='Enable debug logging')
1113 (options, args) = parser.parse_args()
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001114
Jon Salz14bcbb02012-03-17 15:11:50 +08001115 if len(args) != 1:
1116 parser.error('Incorrect number of arguments')
1117
1118 factory.init_logging('ui', verbose=options.verbose)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001119 main(sys.argv[1])