blob: e685a3a344d335a8cbefcecec665151b7c0a5609 [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).
345 @return: A widget with prompt, input entry, and status label.
Chun-ta Lin2e7f44c2012-03-03 07:29:36 +0800346 In addition, a method called get_entry() is added to the widget to
347 provide controls on the entry.
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
396 # Method for getting the entry.
397 widget.get_entry = lambda : entry
Hung-Te Linbf545582012-02-15 17:08:07 +0800398 return widget
399
400
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800401def make_summary_box(tests, state_map, rows=15):
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800402 '''Creates a widget display status of a set of test.
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800403
404 @param tests: A list of FactoryTest nodes whose status (and children's
405 status) should be displayed.
406 @param state_map: The state map as provide by the state instance.
407 @param rows: The number of rows to display.
408 @return: A tuple (widget, label_map), where widget is the widget, and
409 label_map is a map from each test to the corresponding label.
410 '''
411 LABEL_EN_SIZE = (170, 35)
412 LABEL_EN_SIZE_2 = (450, 25)
413 LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
414
415 all_tests = sum([list(t.walk(in_order=True)) for t in tests], [])
416 columns = len(all_tests) / rows + (len(all_tests) % rows != 0)
417
418 info_box = gtk.HBox()
419 info_box.set_spacing(20)
420 for status in (TestState.ACTIVE, TestState.PASSED,
421 TestState.FAILED, TestState.UNTESTED):
422 label = make_label(status,
423 size=LABEL_EN_SIZE,
424 font=LABEL_EN_FONT,
425 alignment=(0.5, 0.5),
426 fg=LABEL_COLORS[status])
427 info_box.pack_start(label, False, False)
428
429 vbox = gtk.VBox()
430 vbox.set_spacing(20)
431 vbox.pack_start(info_box, False, False)
432
433 label_map = {}
434
435 if all_tests:
436 status_table = gtk.Table(rows, columns, True)
437 for (j, i), t in izip(product(xrange(columns), xrange(rows)),
438 all_tests):
439 msg_en = ' ' * (t.depth() - 1) + t.label_en
440 msg_en = trim(msg_en, 12)
441 if t.label_zh:
442 msg = '{0:<12} ({1})'.format(msg_en, t.label_zh)
443 else:
444 msg = msg_en
445 status = state_map[t].status
446 status_label = make_label(msg,
447 size=LABEL_EN_SIZE_2,
448 font=LABEL_EN_FONT,
449 alignment=(0.0, 0.5),
450 fg=LABEL_COLORS[status])
451 label_map[t] = status_label
452 status_table.attach(status_label, j, j+1, i, i+1)
453 vbox.pack_start(status_table, False, False)
454
455 return vbox, label_map
456
457
458def run_test_widget(dummy_job, test_widget,
459 invisible_cursor=True,
460 window_registration_callback=None,
461 cleanup_callback=None):
462 test_widget_size = factory.get_shared_data('test_widget_size')
463
464 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
465 window.modify_bg(gtk.STATE_NORMAL, BLACK)
466 window.set_size_request(*test_widget_size)
467
468 def show_window():
469 window.show()
470 window.window.raise_() # pylint: disable=E1101
471 gtk.gdk.pointer_grab(window.window, confine_to=window.window)
472 if invisible_cursor:
473 hide_cursor(window.window)
474
475 test_path = factory.get_current_test_path()
476
477 def handle_event(event):
478 if (event.type == Event.Type.STATE_CHANGE and
479 test_path and event.path == test_path and
480 event.state.visible):
481 show_window()
482
483 event_client = EventClient(
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800484 callback=handle_event, event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800485
486 align = gtk.Alignment(xalign=0.5, yalign=0.5)
487 align.add(test_widget)
488
489 window.add(align)
490 for c in window.get_children():
491 # Show all children, but not the window itself yet.
492 c.show_all()
493
494 if window_registration_callback is not None:
495 window_registration_callback(window)
496
497 # Show the window if it is the visible test, or if the test_path is not
498 # available (e.g., run directly from the command line).
499 if (not test_path) or (
500 TestState.from_dict_or_object(
501 factory.get_state_instance().get_test_state(test_path)).visible):
502 show_window()
503 else:
504 window.hide()
505
Hung-Te Linde45e9c2012-03-19 13:02:06 +0800506 # When gtk.main() is running, it ignores all uncaught exceptions, which is
507 # not preferred by most of our factory tests. To prevent writing special
508 # function raising errors, we hook top level exception handler to always
509 # leave GTK main and raise exception again.
510
511 def exception_hook(exc_type, value, traceback):
512 # Prevent re-entrant.
513 sys.excepthook = old_excepthook
514 session['exception'] = (exc_type, value, traceback)
515 gobject.idle_add(gtk.main_quit)
516 return old_excepthook(exc_type, value, traceback)
517
518 session = {}
519 old_excepthook = sys.excepthook
520 sys.excepthook = exception_hook
521
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800522 gtk.main()
523
524 gtk.gdk.pointer_ungrab()
525
526 if cleanup_callback is not None:
527 cleanup_callback()
528
529 del event_client
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800530
Hung-Te Linde45e9c2012-03-19 13:02:06 +0800531 sys.excepthook = old_excepthook
532 exc_info = session.get('exception')
533 if exc_info is not None:
534 logging.error(exc_info[0], exc_info=exc_info)
535 raise error.TestError(exc_info[1])
536
537
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800538
539# ---------------------------------------------------------------------------
540# Server Implementation
541
542
543class Console(object):
544 '''Display a progress log. Implemented by launching an borderless
545 xterm at a strategic location, and running tail against the log.'''
546
547 def __init__(self, allocation):
Chun-Ta Lin734e96a2012-03-16 20:05:33 +0800548 # Specify how many lines and characters per line are displayed.
549 XTERM_DISPLAY_LINES = 13
550 XTERM_DISPLAY_CHARS = 120
551 # Extra space reserved for pixels between lines.
552 XTERM_RESERVED_LINES = 3
553
554 xterm_coords = '%dx%d+%d+%d' % (XTERM_DISPLAY_CHARS,
555 XTERM_DISPLAY_LINES,
556 allocation.x,
557 allocation.y)
558 xterm_reserved_height = gtk.gdk.screen_height() - allocation.y
559 font_size = int(float(xterm_reserved_height) / (XTERM_DISPLAY_LINES +
560 XTERM_RESERVED_LINES))
561 logging.info('xterm_reserved_height = %d' % xterm_reserved_height)
562 logging.info('font_size = %d' % font_size)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800563 logging.info('xterm_coords = %s', xterm_coords)
564 xterm_opts = ('-bg black -fg lightgray -bw 0 -g %s' % xterm_coords)
Chun-Ta Lin734e96a2012-03-16 20:05:33 +0800565 xterm_cmd = (
566 ['urxvt'] + xterm_opts.split() +
567 ['-fn', 'xft:DejaVu Sans Mono:pixelsize=%s' % font_size] +
568 ['-e', 'bash'] +
569 ['-c', 'tail -f "%s"' % factory.CONSOLE_LOG_PATH])
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800570 logging.info('xterm_cmd = %s', xterm_cmd)
571 self._proc = subprocess.Popen(xterm_cmd)
572
573 def __del__(self):
574 logging.info('console_proc __del__')
575 self._proc.kill()
576
577
578class TestLabelBox(gtk.EventBox): # pylint: disable=R0904
579
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800580 def __init__(self, test):
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800581 gtk.EventBox.__init__(self)
582 self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[TestState.UNTESTED])
Hung-Te Line94e0a02012-03-19 18:20:35 +0800583 self._is_group = test.is_group()
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800584 depth = len(test.get_ancestor_groups())
Hung-Te Line94e0a02012-03-19 18:20:35 +0800585 self._label_text = ' %s%s%s' % (
586 ' ' * depth,
587 SYMBOL_RIGHT_ARROW if self._is_group else ' ',
588 test.label_en)
589 if self._is_group:
590 self._label_text_collapsed = ' %s%s%s' % (
591 ' ' * depth,
592 SYMBOL_DOWN_ARROW if self._is_group else '',
593 test.label_en)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800594 self._label_en = make_label(
Hung-Te Line94e0a02012-03-19 18:20:35 +0800595 self._label_text, size=_LABEL_EN_SIZE,
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800596 font=_LABEL_EN_FONT, alignment=(0, 0.5),
597 fg=_LABEL_UNTESTED_FG)
598 self._label_zh = make_label(
599 test.label_zh, size=_LABEL_ZH_SIZE,
600 font=_LABEL_ZH_FONT, alignment=(0.5, 0.5),
601 fg=_LABEL_UNTESTED_FG)
602 self._label_t = make_label(
603 '', size=_LABEL_T_SIZE, font=_LABEL_T_FONT,
604 alignment=(0.5, 0.5), fg=BLACK)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800605 hbox = gtk.HBox()
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800606 hbox.pack_start(self._label_en, False, False)
607 hbox.pack_start(self._label_zh, False, False)
608 hbox.pack_start(self._label_t, False, False)
609 vbox = gtk.VBox()
610 vbox.pack_start(hbox, False, False)
611 vbox.pack_start(make_hsep(), False, False)
612 self.add(vbox)
613 self._status = None
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800614
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800615 def set_shortcut(self, shortcut):
616 if shortcut is None:
617 return
Hung-Te Lin99f64ef2012-03-22 11:05:45 +0800618 self._label_t.set_text('C-%s' % shortcut.upper())
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800619 attrs = self._label_en.get_attributes() or pango.AttrList()
620 attrs.filter(lambda attr: attr.type == pango.ATTR_UNDERLINE)
621 index_hotkey = self._label_en.get_text().upper().find(shortcut.upper())
622 if index_hotkey != -1:
623 attrs.insert(pango.AttrUnderline(
624 pango.UNDERLINE_LOW, index_hotkey, index_hotkey + 1))
625 attrs.insert(pango.AttrWeight(
626 pango.WEIGHT_BOLD, index_hotkey, index_hotkey + 1))
627 self._label_en.set_attributes(attrs)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800628 self.queue_draw()
629
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800630 def update(self, status):
631 if self._status == status:
632 return
633 self._status = status
634 label_fg = (_LABEL_UNTESTED_FG if status == TestState.UNTESTED
635 else BLACK)
Hung-Te Line94e0a02012-03-19 18:20:35 +0800636 if self._is_group:
637 self._label_en.set_text(
638 self._label_text_collapsed if status == TestState.ACTIVE
639 else self._label_text)
640
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800641 for label in [self._label_en, self._label_zh, self._label_t]:
642 label.modify_fg(gtk.STATE_NORMAL, label_fg)
643 self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[status])
644 self.queue_draw()
645
646
Hung-Te Lin96632362012-03-20 21:14:18 +0800647class ReviewInformation(object):
648
649 LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
650 TAB_BORDER = 20
651
652 def __init__(self, test_list):
653 self.test_list = test_list
654
655 def make_error_tab(self, test, state):
656 msg = '%s (%s)\n%s' % (test.label_en, test.label_zh,
657 str(state.error_msg))
658 label = make_label(msg, font=self.LABEL_EN_FONT, alignment=(0.0, 0.0))
659 label.set_line_wrap(True)
660 frame = gtk.Frame()
661 frame.add(label)
662 return frame
663
664 def make_widget(self):
665 bg_color = gtk.gdk.Color(0x1000, 0x1000, 0x1000)
666 self.notebook = gtk.Notebook()
667 self.notebook.modify_bg(gtk.STATE_NORMAL, bg_color)
668
669 test_list = self.test_list
670 state_map = test_list.get_state_map()
671 tab, _ = make_summary_box([test_list], state_map)
672 tab.set_border_width(self.TAB_BORDER)
673 self.notebook.append_page(tab, make_label('Summary'))
674
675 for i, t in izip(
676 count(1),
677 [t for t in test_list.walk()
678 if state_map[t].status == factory.TestState.FAILED
679 and t.is_leaf()]):
680 tab = self.make_error_tab(t, state_map[t])
681 tab.set_border_width(self.TAB_BORDER)
682 self.notebook.append_page(tab, make_label('#%02d' % i))
683
684 prompt = 'Review: Test Status Information'
685 if self.notebook.get_n_pages() > 1:
686 prompt += '\nPress left/right to change tabs'
687
688 control_label = make_label(prompt, font=self.LABEL_EN_FONT,
689 alignment=(0.5, 0.5))
690 vbox = gtk.VBox()
691 vbox.set_spacing(self.TAB_BORDER)
692 vbox.pack_start(control_label, False, False)
693 vbox.pack_start(self.notebook, False, False)
694 vbox.show_all()
695 vbox.grab_focus = self.notebook.grab_focus
696 return vbox
697
698
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800699class TestDirectory(gtk.VBox):
700 '''Widget containing a list of tests, colored by test status.
701
702 This is the widget corresponding to the RHS test panel.
703
704 Attributes:
705 _label_map: Dict of test path to TestLabelBox objects. Should
706 contain an entry for each test that has been visible at some
707 time.
708 _visible_status: List of (test, status) pairs reflecting the
709 last refresh of the set of visible tests. This is used to
710 rememeber what tests were active, to allow implementation of
711 visual refresh only when new active tests appear.
712 _shortcut_map: Dict of keyboard shortcut key to test path.
713 Tracks the current set of keyboard shortcut mappings for the
714 visible set of tests. This will change when the visible
715 test set changes.
716 '''
717
Tammo Spalinkddae4df2012-03-22 11:50:00 +0800718 def __init__(self, test_list):
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800719 gtk.VBox.__init__(self)
720 self.set_spacing(0)
721 self._label_map = {}
722 self._visible_status = []
723 self._shortcut_map = {}
Tammo Spalinkddae4df2012-03-22 11:50:00 +0800724 self._hard_shortcuts = set(
725 test.kbd_shortcut for test in test_list.walk()
726 if test.kbd_shortcut is not None)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800727
728 def _get_test_label(self, test):
729 if test.path in self._label_map:
730 return self._label_map[test.path]
731 label_box = TestLabelBox(test)
732 self._label_map[test.path] = label_box
733 return label_box
734
735 def _remove_shortcut(self, path):
736 reverse_map = dict((v, k) for k, v in self._shortcut_map.items())
737 if path not in reverse_map:
738 logging.error('Removal of non-present shortcut for %s' % path)
739 return
740 shortcut = reverse_map[path]
741 del self._shortcut_map[shortcut]
742
743 def _add_shortcut(self, test):
744 shortcut = test.kbd_shortcut
745 if shortcut in self._shortcut_map:
746 logging.error('Shortcut %s already in use by %s; cannot apply to %s'
747 % (shortcut, self._shortcut_map[shortcut], test.path))
748 shortcut = None
749 if shortcut is None:
750 # Find a suitable shortcut. For groups, use numbers. For
751 # regular tests, use alpha (letters).
Jon Salz0405ab52012-03-16 15:26:52 +0800752 if test.is_group():
753 gen = (x for x in string.digits if x not in self._shortcut_map)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800754 else:
Jon Salz0405ab52012-03-16 15:26:52 +0800755 gen = (x for x in test.label_en.lower() + string.lowercase
Tammo Spalinkddae4df2012-03-22 11:50:00 +0800756 if x.isalnum() and x not in self._shortcut_map
757 and x not in self._hard_shortcuts)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800758 shortcut = next(gen, None)
759 if shortcut is None:
760 logging.error('Unable to find shortcut for %s' % test.path)
761 return
762 self._shortcut_map[shortcut] = test.path
763 return shortcut
764
765 def handle_xevent(self, dummy_src, dummy_cond,
766 xhandle, keycode_map, event_client):
767 for dummy_i in range(0, xhandle.pending_events()):
768 xevent = xhandle.next_event()
769 if xevent.type != X.KeyPress:
770 continue
771 keycode = xevent.detail
772 if keycode not in keycode_map:
773 logging.warning('Ignoring unknown keycode %r' % keycode)
774 continue
775 shortcut = keycode_map[keycode]
Jon Salz0405ab52012-03-16 15:26:52 +0800776
Hung-Te Lin96632362012-03-20 21:14:18 +0800777 if (xevent.state & GLOBAL_HOT_KEY_MASK == GLOBAL_HOT_KEY_MASK):
Jon Salz0405ab52012-03-16 15:26:52 +0800778 event_type = GLOBAL_HOT_KEY_EVENTS.get(shortcut)
779 if event_type:
780 event_client.post_event(Event(event_type))
781 else:
782 logging.warning('Unbound global hot key %s' % key)
783 else:
784 if shortcut not in self._shortcut_map:
785 logging.warning('Ignoring unbound shortcut %r' % shortcut)
786 continue
787 test_path = self._shortcut_map[shortcut]
788 event_client.post_event(Event(Event.Type.SWITCH_TEST,
789 path=test_path))
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800790 return True
791
792 def update(self, new_test_status):
793 '''Refresh the RHS test list to show current status and active groups.
794
795 Refresh the set of visible tests only when new active tests
796 arise. This avoids visual volatility when switching between
797 tests (intervals where no test is active). Also refresh at
798 initial startup.
799
800 Args:
801 new_test_status: A list of (test, status) tuples. The tests
802 order should match how they should be displayed in the
803 directory (rhs panel).
804 '''
805 old_active = set(t for t, s in self._visible_status
806 if s == TestState.ACTIVE)
807 new_active = set(t for t, s in new_test_status
808 if s == TestState.ACTIVE)
809 new_visible = set(t for t, s in new_test_status)
810 old_visible = set(t for t, s in self._visible_status)
811
812 if old_active and not new_active - old_active:
813 # No new active tests, so do not change the displayed test
814 # set, only update the displayed status for currently
815 # visible tests. Not updating _visible_status allows us
816 # to remember the last set of active tests.
817 for test, _ in self._visible_status:
818 status = test.get_state().status
819 self._label_map[test.path].update(status)
820 return
821
822 self._visible_status = new_test_status
823
824 new_test_map = dict((t.path, t) for t, s in new_test_status)
825
826 for test in old_visible - new_visible:
827 label_box = self._label_map[test.path]
828 logging.debug('removing %s test label' % test.path)
829 self.remove(label_box)
830 self._remove_shortcut(test.path)
831
832 new_tests = new_visible - old_visible
833
834 for position, (test, status) in enumerate(new_test_status):
835 label_box = self._get_test_label(test)
836 if test in new_tests:
837 shortcut = self._add_shortcut(test)
838 label_box = self._get_test_label(test)
839 label_box.set_shortcut(shortcut)
840 logging.debug('adding %s test label (sortcut %r, pos %d)' %
841 (test.path, shortcut, position))
842 self.pack_start(label_box, False, False)
843 self.reorder_child(label_box, position)
844 label_box.update(status)
845
846 self.show_all()
847
848
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800849
850class UiState(object):
851
Hung-Te Lin96632362012-03-20 21:14:18 +0800852 WIDGET_NONE = 0
853 WIDGET_IDLE = 1
854 WIDGET_SUMMARY = 2
855 WIDGET_REVIEW = 3
856
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800857 def __init__(self, test_widget_box, test_directory_widget, test_list):
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800858 self._test_widget_box = test_widget_box
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800859 self._test_directory_widget = test_directory_widget
860 self._test_list = test_list
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800861 self._transition_count = 0
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800862 self._active_test_label_map = None
Hung-Te Lin96632362012-03-20 21:14:18 +0800863 self._active_widget = self.WIDGET_NONE
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800864 self.update_test_state()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800865
Hung-Te Lin96632362012-03-20 21:14:18 +0800866 def show_idle_widget(self):
867 self.remove_state_widget()
868 self._test_widget_box.set(0.5, 0.5, 0.0, 0.0)
869 self._test_widget_box.set_padding(0, 0, 0, 0)
870 label = make_label(MESSAGE_NO_ACTIVE_TESTS,
871 font=_OTHER_LABEL_FONT,
872 alignment=(0.5, 0.5))
873 self._test_widget_box.add(label)
874 self._test_widget_box.show_all()
875 self._active_widget = self.WIDGET_IDLE
876
877 def show_summary_widget(self):
878 self.remove_state_widget()
879 state_map = self._test_list.get_state_map()
880 self._test_widget_box.set(0.5, 0.0, 0.0, 0.0)
881 self._test_widget_box.set_padding(40, 0, 0, 0)
882 vbox, self._active_test_label_map = make_summary_box(
883 [t for t in self._test_list.subtests
884 if state_map[t].status == TestState.ACTIVE],
885 state_map)
886 self._test_widget_box.add(vbox)
887 self._test_widget_box.show_all()
888 self._active_widget = self.WIDGET_SUMMARY
889
890 def show_review_widget(self):
891 self.remove_state_widget()
892 self._review_request = False
893 self._test_widget_box.set(0.5, 0.5, 0.0, 0.0)
894 self._test_widget_box.set_padding(0, 0, 0, 0)
895 widget = ReviewInformation(self._test_list).make_widget()
896 self._test_widget_box.add(widget)
897 self._test_widget_box.show_all()
898 widget.grab_focus()
899 self._active_widget = self.WIDGET_REVIEW
900
901 def remove_state_widget(self):
902 for child in self._test_widget_box.get_children():
903 child.hide()
904 self._test_widget_box.remove(child)
905 self._active_test_label_map = None
906 self._active_widget = self.WIDGET_NONE
907
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800908 def update_test_state(self):
909 state_map = self._test_list.get_state_map()
910 active_tests = set(
911 t for t in self._test_list.walk()
912 if t.is_leaf() and state_map[t].status == TestState.ACTIVE)
913 active_groups = set(g for t in active_tests
914 for g in t.get_ancestor_groups())
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800915
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800916 def filter_visible_test_state(tests):
917 '''List currently visible tests and their status.
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800918
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800919 Visible means currently displayed in the RHS panel.
920 Visiblity is implied by being a top level test or having
921 membership in a group with at least one active test.
922
923 Returns:
924 A list of (test, status) tuples for all visible tests,
925 in the order they should be displayed.
926 '''
927 results = []
928 for test in tests:
Jon Salz0405ab52012-03-16 15:26:52 +0800929 if test.is_group():
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800930 results.append((test, TestState.UNTESTED))
931 if test not in active_groups:
932 continue
933 results += filter_visible_test_state(test.subtests)
934 else:
935 results.append((test, state_map[test].status))
936 return results
937
938 visible_test_state = filter_visible_test_state(self._test_list.subtests)
939 self._test_directory_widget.update(visible_test_state)
940
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800941 if not active_tests:
Hung-Te Lin96632362012-03-20 21:14:18 +0800942 # Display the idle or review information screen.
943 def waiting_for_transition():
944 return (self._active_widget not in
945 [self.WIDGET_REVIEW, self.WIDGET_IDLE])
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800946
Hung-Te Lin96632362012-03-20 21:14:18 +0800947 # For smooth transition between tests, idle widget if activated only
948 # after _NO_ACTIVE_TEST_DELAY_MS without state change.
949 def idle_transition_check(cookie):
950 if (waiting_for_transition() and
951 cookie == self._transition_count):
952 self._transition_count += 1
953 self.show_idle_widget()
954 return False
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800955
Hung-Te Lin96632362012-03-20 21:14:18 +0800956 if waiting_for_transition():
957 gobject.timeout_add(_NO_ACTIVE_TEST_DELAY_MS,
958 idle_transition_check,
959 self._transition_count)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800960 return
961
962 self._transition_count += 1
963
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800964 if any(t.has_ui for t in active_tests):
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800965 # Remove the widget (if any) since there is an active test
966 # with a UI.
Hung-Te Lin96632362012-03-20 21:14:18 +0800967 self.remove_state_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800968 return
969
970 if (self._active_test_label_map is not None and
971 all(t in self._active_test_label_map for t in active_tests)):
972 # All active tests are already present in the summary, so just
973 # update their states.
974 for test, label in self._active_test_label_map.iteritems():
975 label.modify_fg(
976 gtk.STATE_NORMAL,
977 LABEL_COLORS[state_map[test].status])
978 return
979
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800980 # No active UI; draw summary of current test states
Hung-Te Lin96632362012-03-20 21:14:18 +0800981 self.show_summary_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800982
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800983
984def grab_shortcut_keys(disp, event_handler, event_client):
985 # We want to receive KeyPress events
986 root = disp.screen().root
987 root.change_attributes(event_mask = X.KeyPressMask)
988 shortcut_set = set(string.lowercase + string.digits)
989 keycode_map = {}
990 for mod, shortcut in ([(X.ControlMask, k) for k in shortcut_set] +
Jon Salz0405ab52012-03-16 15:26:52 +0800991 [(GLOBAL_HOT_KEY_MASK, k)
992 for k in GLOBAL_HOT_KEY_EVENTS] +
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800993 [(X.Mod1Mask, 'Tab')]): # Mod1 = Alt
994 keysym = gtk.gdk.keyval_from_name(shortcut)
995 keycode = disp.keysym_to_keycode(keysym)
996 keycode_map[keycode] = shortcut
997 root.grab_key(keycode, mod, 1, X.GrabModeAsync, X.GrabModeAsync)
998 # This flushes the XGrabKey calls to the server.
999 for dummy_x in range(0, root.display.pending_events()):
1000 root.display.next_event()
1001 gobject.io_add_watch(root.display, gobject.IO_IN, event_handler,
1002 root.display, keycode_map, event_client)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001003
1004
1005def main(test_list_path):
1006 '''Starts the main UI.
1007
1008 This is launched by the autotest/cros/factory/client.
1009 When operators press keyboard shortcuts, the shortcut
1010 value is sent as an event to the control program.'''
1011
1012 test_list = None
1013 ui_state = None
1014 event_client = None
1015
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001016 def handle_key_release_event(_, event):
1017 logging.info('base ui key event (%s)', event.keyval)
1018 return True
1019
1020 def handle_event(event):
1021 if event.type == Event.Type.STATE_CHANGE:
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001022 ui_state.update_test_state()
Hung-Te Lin96632362012-03-20 21:14:18 +08001023 elif event.type == Event.Type.REVIEW:
1024 logging.info("Operator activates review information screen")
1025 ui_state.show_review_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001026
1027 test_list = factory.read_test_list(test_list_path)
1028
1029 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
1030 window.connect('destroy', lambda _: gtk.main_quit())
1031 window.modify_bg(gtk.STATE_NORMAL, BLACK)
1032
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001033 disp = Display()
1034
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001035 event_client = EventClient(
1036 callback=handle_event,
1037 event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
1038
1039 screen = window.get_screen()
1040 if (screen is None):
1041 logging.info('ERROR: communication with the X server is not working, ' +
1042 'could not find a working screen. UI exiting.')
1043 sys.exit(1)
1044
1045 screen_size_str = os.environ.get('CROS_SCREEN_SIZE')
1046 if screen_size_str:
1047 match = re.match(r'^(\d+)x(\d+)$', screen_size_str)
1048 assert match, 'CROS_SCREEN_SIZE should be {width}x{height}'
1049 screen_size = (int(match.group(1)), int(match.group(2)))
1050 else:
1051 screen_size = (screen.get_width(), screen.get_height())
1052 window.set_size_request(*screen_size)
1053
Tammo Spalinkddae4df2012-03-22 11:50:00 +08001054 test_directory = TestDirectory(test_list)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001055
1056 rhs_box = gtk.EventBox()
1057 rhs_box.modify_bg(gtk.STATE_NORMAL, _LABEL_TROUGH_COLOR)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001058 rhs_box.add(test_directory)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001059
1060 console_box = gtk.EventBox()
Chun-Ta Lin734e96a2012-03-16 20:05:33 +08001061 console_box.set_size_request(*convert_pixels((-1, 180)))
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001062 console_box.modify_bg(gtk.STATE_NORMAL, BLACK)
1063
1064 test_widget_box = gtk.Alignment()
1065 test_widget_box.set_size_request(-1, -1)
1066
1067 lhs_box = gtk.VBox()
1068 lhs_box.pack_end(console_box, False, False)
1069 lhs_box.pack_start(test_widget_box)
1070 lhs_box.pack_start(make_hsep(3), False, False)
1071
1072 base_box = gtk.HBox()
1073 base_box.pack_end(rhs_box, False, False)
1074 base_box.pack_end(make_vsep(3), False, False)
1075 base_box.pack_start(lhs_box)
1076
1077 window.connect('key-release-event', handle_key_release_event)
1078 window.add_events(gtk.gdk.KEY_RELEASE_MASK)
1079
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001080 ui_state = UiState(test_widget_box, test_directory, test_list)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001081
1082 window.add(base_box)
1083 window.show_all()
1084
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001085 grab_shortcut_keys(disp, test_directory.handle_xevent, event_client)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001086
1087 hide_cursor(window.window)
1088
1089 test_widget_allocation = test_widget_box.get_allocation()
1090 test_widget_size = (test_widget_allocation.width,
1091 test_widget_allocation.height)
1092 factory.set_shared_data('test_widget_size', test_widget_size)
1093
Jon Salz758e6cc2012-04-03 15:47:07 +08001094 if not factory.in_chroot():
1095 dummy_console = Console(console_box.get_allocation())
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001096
1097 event_client.post_event(Event(Event.Type.UI_READY))
1098
1099 logging.info('cros/factory/ui setup done, starting gtk.main()...')
1100 gtk.main()
1101 logging.info('cros/factory/ui gtk.main() finished, exiting.')
1102
1103
1104if __name__ == '__main__':
Jon Salz14bcbb02012-03-17 15:11:50 +08001105 parser = OptionParser(usage='usage: %prog [options] TEST-LIST-PATH')
1106 parser.add_option('-v', '--verbose', dest='verbose',
1107 action='store_true',
1108 help='Enable debug logging')
1109 (options, args) = parser.parse_args()
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001110
Jon Salz14bcbb02012-03-17 15:11:50 +08001111 if len(args) != 1:
1112 parser.error('Incorrect number of arguments')
1113
1114 factory.init_logging('ui', verbose=options.verbose)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001115 main(sys.argv[1])