blob: 4218f9cde8095713ca66950a53076c405f5fb0c5 [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
93FAIL_TIMEOUT = 30
94
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
618 self._label_t.set_text('C-%s' % shortcut)
619 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
718 def __init__(self):
719 gtk.VBox.__init__(self)
720 self.set_spacing(0)
721 self._label_map = {}
722 self._visible_status = []
723 self._shortcut_map = {}
724
725 def _get_test_label(self, test):
726 if test.path in self._label_map:
727 return self._label_map[test.path]
728 label_box = TestLabelBox(test)
729 self._label_map[test.path] = label_box
730 return label_box
731
732 def _remove_shortcut(self, path):
733 reverse_map = dict((v, k) for k, v in self._shortcut_map.items())
734 if path not in reverse_map:
735 logging.error('Removal of non-present shortcut for %s' % path)
736 return
737 shortcut = reverse_map[path]
738 del self._shortcut_map[shortcut]
739
740 def _add_shortcut(self, test):
741 shortcut = test.kbd_shortcut
742 if shortcut in self._shortcut_map:
743 logging.error('Shortcut %s already in use by %s; cannot apply to %s'
744 % (shortcut, self._shortcut_map[shortcut], test.path))
745 shortcut = None
746 if shortcut is None:
747 # Find a suitable shortcut. For groups, use numbers. For
748 # regular tests, use alpha (letters).
Jon Salz0405ab52012-03-16 15:26:52 +0800749 if test.is_group():
750 gen = (x for x in string.digits if x not in self._shortcut_map)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800751 else:
Jon Salz0405ab52012-03-16 15:26:52 +0800752 gen = (x for x in test.label_en.lower() + string.lowercase
753 if x.isalnum() and x not in self._shortcut_map)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800754 shortcut = next(gen, None)
755 if shortcut is None:
756 logging.error('Unable to find shortcut for %s' % test.path)
757 return
758 self._shortcut_map[shortcut] = test.path
759 return shortcut
760
761 def handle_xevent(self, dummy_src, dummy_cond,
762 xhandle, keycode_map, event_client):
763 for dummy_i in range(0, xhandle.pending_events()):
764 xevent = xhandle.next_event()
765 if xevent.type != X.KeyPress:
766 continue
767 keycode = xevent.detail
768 if keycode not in keycode_map:
769 logging.warning('Ignoring unknown keycode %r' % keycode)
770 continue
771 shortcut = keycode_map[keycode]
Jon Salz0405ab52012-03-16 15:26:52 +0800772
Hung-Te Lin96632362012-03-20 21:14:18 +0800773 if (xevent.state & GLOBAL_HOT_KEY_MASK == GLOBAL_HOT_KEY_MASK):
Jon Salz0405ab52012-03-16 15:26:52 +0800774 event_type = GLOBAL_HOT_KEY_EVENTS.get(shortcut)
775 if event_type:
776 event_client.post_event(Event(event_type))
777 else:
778 logging.warning('Unbound global hot key %s' % key)
779 else:
780 if shortcut not in self._shortcut_map:
781 logging.warning('Ignoring unbound shortcut %r' % shortcut)
782 continue
783 test_path = self._shortcut_map[shortcut]
784 event_client.post_event(Event(Event.Type.SWITCH_TEST,
785 path=test_path))
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800786 return True
787
788 def update(self, new_test_status):
789 '''Refresh the RHS test list to show current status and active groups.
790
791 Refresh the set of visible tests only when new active tests
792 arise. This avoids visual volatility when switching between
793 tests (intervals where no test is active). Also refresh at
794 initial startup.
795
796 Args:
797 new_test_status: A list of (test, status) tuples. The tests
798 order should match how they should be displayed in the
799 directory (rhs panel).
800 '''
801 old_active = set(t for t, s in self._visible_status
802 if s == TestState.ACTIVE)
803 new_active = set(t for t, s in new_test_status
804 if s == TestState.ACTIVE)
805 new_visible = set(t for t, s in new_test_status)
806 old_visible = set(t for t, s in self._visible_status)
807
808 if old_active and not new_active - old_active:
809 # No new active tests, so do not change the displayed test
810 # set, only update the displayed status for currently
811 # visible tests. Not updating _visible_status allows us
812 # to remember the last set of active tests.
813 for test, _ in self._visible_status:
814 status = test.get_state().status
815 self._label_map[test.path].update(status)
816 return
817
818 self._visible_status = new_test_status
819
820 new_test_map = dict((t.path, t) for t, s in new_test_status)
821
822 for test in old_visible - new_visible:
823 label_box = self._label_map[test.path]
824 logging.debug('removing %s test label' % test.path)
825 self.remove(label_box)
826 self._remove_shortcut(test.path)
827
828 new_tests = new_visible - old_visible
829
830 for position, (test, status) in enumerate(new_test_status):
831 label_box = self._get_test_label(test)
832 if test in new_tests:
833 shortcut = self._add_shortcut(test)
834 label_box = self._get_test_label(test)
835 label_box.set_shortcut(shortcut)
836 logging.debug('adding %s test label (sortcut %r, pos %d)' %
837 (test.path, shortcut, position))
838 self.pack_start(label_box, False, False)
839 self.reorder_child(label_box, position)
840 label_box.update(status)
841
842 self.show_all()
843
844
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800845
846class UiState(object):
847
Hung-Te Lin96632362012-03-20 21:14:18 +0800848 WIDGET_NONE = 0
849 WIDGET_IDLE = 1
850 WIDGET_SUMMARY = 2
851 WIDGET_REVIEW = 3
852
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800853 def __init__(self, test_widget_box, test_directory_widget, test_list):
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800854 self._test_widget_box = test_widget_box
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800855 self._test_directory_widget = test_directory_widget
856 self._test_list = test_list
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800857 self._transition_count = 0
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800858 self._active_test_label_map = None
Hung-Te Lin96632362012-03-20 21:14:18 +0800859 self._active_widget = self.WIDGET_NONE
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800860 self.update_test_state()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800861
Hung-Te Lin96632362012-03-20 21:14:18 +0800862 def show_idle_widget(self):
863 self.remove_state_widget()
864 self._test_widget_box.set(0.5, 0.5, 0.0, 0.0)
865 self._test_widget_box.set_padding(0, 0, 0, 0)
866 label = make_label(MESSAGE_NO_ACTIVE_TESTS,
867 font=_OTHER_LABEL_FONT,
868 alignment=(0.5, 0.5))
869 self._test_widget_box.add(label)
870 self._test_widget_box.show_all()
871 self._active_widget = self.WIDGET_IDLE
872
873 def show_summary_widget(self):
874 self.remove_state_widget()
875 state_map = self._test_list.get_state_map()
876 self._test_widget_box.set(0.5, 0.0, 0.0, 0.0)
877 self._test_widget_box.set_padding(40, 0, 0, 0)
878 vbox, self._active_test_label_map = make_summary_box(
879 [t for t in self._test_list.subtests
880 if state_map[t].status == TestState.ACTIVE],
881 state_map)
882 self._test_widget_box.add(vbox)
883 self._test_widget_box.show_all()
884 self._active_widget = self.WIDGET_SUMMARY
885
886 def show_review_widget(self):
887 self.remove_state_widget()
888 self._review_request = False
889 self._test_widget_box.set(0.5, 0.5, 0.0, 0.0)
890 self._test_widget_box.set_padding(0, 0, 0, 0)
891 widget = ReviewInformation(self._test_list).make_widget()
892 self._test_widget_box.add(widget)
893 self._test_widget_box.show_all()
894 widget.grab_focus()
895 self._active_widget = self.WIDGET_REVIEW
896
897 def remove_state_widget(self):
898 for child in self._test_widget_box.get_children():
899 child.hide()
900 self._test_widget_box.remove(child)
901 self._active_test_label_map = None
902 self._active_widget = self.WIDGET_NONE
903
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800904 def update_test_state(self):
905 state_map = self._test_list.get_state_map()
906 active_tests = set(
907 t for t in self._test_list.walk()
908 if t.is_leaf() and state_map[t].status == TestState.ACTIVE)
909 active_groups = set(g for t in active_tests
910 for g in t.get_ancestor_groups())
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800911
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800912 def filter_visible_test_state(tests):
913 '''List currently visible tests and their status.
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800914
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800915 Visible means currently displayed in the RHS panel.
916 Visiblity is implied by being a top level test or having
917 membership in a group with at least one active test.
918
919 Returns:
920 A list of (test, status) tuples for all visible tests,
921 in the order they should be displayed.
922 '''
923 results = []
924 for test in tests:
Jon Salz0405ab52012-03-16 15:26:52 +0800925 if test.is_group():
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800926 results.append((test, TestState.UNTESTED))
927 if test not in active_groups:
928 continue
929 results += filter_visible_test_state(test.subtests)
930 else:
931 results.append((test, state_map[test].status))
932 return results
933
934 visible_test_state = filter_visible_test_state(self._test_list.subtests)
935 self._test_directory_widget.update(visible_test_state)
936
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800937 if not active_tests:
Hung-Te Lin96632362012-03-20 21:14:18 +0800938 # Display the idle or review information screen.
939 def waiting_for_transition():
940 return (self._active_widget not in
941 [self.WIDGET_REVIEW, self.WIDGET_IDLE])
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800942
Hung-Te Lin96632362012-03-20 21:14:18 +0800943 # For smooth transition between tests, idle widget if activated only
944 # after _NO_ACTIVE_TEST_DELAY_MS without state change.
945 def idle_transition_check(cookie):
946 if (waiting_for_transition() and
947 cookie == self._transition_count):
948 self._transition_count += 1
949 self.show_idle_widget()
950 return False
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800951
Hung-Te Lin96632362012-03-20 21:14:18 +0800952 if waiting_for_transition():
953 gobject.timeout_add(_NO_ACTIVE_TEST_DELAY_MS,
954 idle_transition_check,
955 self._transition_count)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800956 return
957
958 self._transition_count += 1
959
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800960 if any(t.has_ui for t in active_tests):
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800961 # Remove the widget (if any) since there is an active test
962 # with a UI.
Hung-Te Lin96632362012-03-20 21:14:18 +0800963 self.remove_state_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800964 return
965
966 if (self._active_test_label_map is not None and
967 all(t in self._active_test_label_map for t in active_tests)):
968 # All active tests are already present in the summary, so just
969 # update their states.
970 for test, label in self._active_test_label_map.iteritems():
971 label.modify_fg(
972 gtk.STATE_NORMAL,
973 LABEL_COLORS[state_map[test].status])
974 return
975
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800976 # No active UI; draw summary of current test states
Hung-Te Lin96632362012-03-20 21:14:18 +0800977 self.show_summary_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800978
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800979
980def grab_shortcut_keys(disp, event_handler, event_client):
981 # We want to receive KeyPress events
982 root = disp.screen().root
983 root.change_attributes(event_mask = X.KeyPressMask)
984 shortcut_set = set(string.lowercase + string.digits)
985 keycode_map = {}
986 for mod, shortcut in ([(X.ControlMask, k) for k in shortcut_set] +
Jon Salz0405ab52012-03-16 15:26:52 +0800987 [(GLOBAL_HOT_KEY_MASK, k)
988 for k in GLOBAL_HOT_KEY_EVENTS] +
Tammo Spalinkdb8d7112012-03-13 18:54:37 +0800989 [(X.Mod1Mask, 'Tab')]): # Mod1 = Alt
990 keysym = gtk.gdk.keyval_from_name(shortcut)
991 keycode = disp.keysym_to_keycode(keysym)
992 keycode_map[keycode] = shortcut
993 root.grab_key(keycode, mod, 1, X.GrabModeAsync, X.GrabModeAsync)
994 # This flushes the XGrabKey calls to the server.
995 for dummy_x in range(0, root.display.pending_events()):
996 root.display.next_event()
997 gobject.io_add_watch(root.display, gobject.IO_IN, event_handler,
998 root.display, keycode_map, event_client)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800999
1000
1001def main(test_list_path):
1002 '''Starts the main UI.
1003
1004 This is launched by the autotest/cros/factory/client.
1005 When operators press keyboard shortcuts, the shortcut
1006 value is sent as an event to the control program.'''
1007
1008 test_list = None
1009 ui_state = None
1010 event_client = None
1011
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001012 def handle_key_release_event(_, event):
1013 logging.info('base ui key event (%s)', event.keyval)
1014 return True
1015
1016 def handle_event(event):
1017 if event.type == Event.Type.STATE_CHANGE:
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001018 ui_state.update_test_state()
Hung-Te Lin96632362012-03-20 21:14:18 +08001019 elif event.type == Event.Type.REVIEW:
1020 logging.info("Operator activates review information screen")
1021 ui_state.show_review_widget()
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001022
1023 test_list = factory.read_test_list(test_list_path)
1024
1025 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
1026 window.connect('destroy', lambda _: gtk.main_quit())
1027 window.modify_bg(gtk.STATE_NORMAL, BLACK)
1028
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001029 disp = Display()
1030
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001031 event_client = EventClient(
1032 callback=handle_event,
1033 event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
1034
1035 screen = window.get_screen()
1036 if (screen is None):
1037 logging.info('ERROR: communication with the X server is not working, ' +
1038 'could not find a working screen. UI exiting.')
1039 sys.exit(1)
1040
1041 screen_size_str = os.environ.get('CROS_SCREEN_SIZE')
1042 if screen_size_str:
1043 match = re.match(r'^(\d+)x(\d+)$', screen_size_str)
1044 assert match, 'CROS_SCREEN_SIZE should be {width}x{height}'
1045 screen_size = (int(match.group(1)), int(match.group(2)))
1046 else:
1047 screen_size = (screen.get_width(), screen.get_height())
1048 window.set_size_request(*screen_size)
1049
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001050 test_directory = TestDirectory()
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001051
1052 rhs_box = gtk.EventBox()
1053 rhs_box.modify_bg(gtk.STATE_NORMAL, _LABEL_TROUGH_COLOR)
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001054 rhs_box.add(test_directory)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001055
1056 console_box = gtk.EventBox()
Chun-Ta Lin734e96a2012-03-16 20:05:33 +08001057 console_box.set_size_request(*convert_pixels((-1, 180)))
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001058 console_box.modify_bg(gtk.STATE_NORMAL, BLACK)
1059
1060 test_widget_box = gtk.Alignment()
1061 test_widget_box.set_size_request(-1, -1)
1062
1063 lhs_box = gtk.VBox()
1064 lhs_box.pack_end(console_box, False, False)
1065 lhs_box.pack_start(test_widget_box)
1066 lhs_box.pack_start(make_hsep(3), False, False)
1067
1068 base_box = gtk.HBox()
1069 base_box.pack_end(rhs_box, False, False)
1070 base_box.pack_end(make_vsep(3), False, False)
1071 base_box.pack_start(lhs_box)
1072
1073 window.connect('key-release-event', handle_key_release_event)
1074 window.add_events(gtk.gdk.KEY_RELEASE_MASK)
1075
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001076 ui_state = UiState(test_widget_box, test_directory, test_list)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001077
1078 window.add(base_box)
1079 window.show_all()
1080
Tammo Spalinkdb8d7112012-03-13 18:54:37 +08001081 grab_shortcut_keys(disp, test_directory.handle_xevent, event_client)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001082
1083 hide_cursor(window.window)
1084
1085 test_widget_allocation = test_widget_box.get_allocation()
1086 test_widget_size = (test_widget_allocation.width,
1087 test_widget_allocation.height)
1088 factory.set_shared_data('test_widget_size', test_widget_size)
1089
1090 dummy_console = Console(console_box.get_allocation())
1091
1092 event_client.post_event(Event(Event.Type.UI_READY))
1093
1094 logging.info('cros/factory/ui setup done, starting gtk.main()...')
1095 gtk.main()
1096 logging.info('cros/factory/ui gtk.main() finished, exiting.')
1097
1098
1099if __name__ == '__main__':
Jon Salz14bcbb02012-03-17 15:11:50 +08001100 parser = OptionParser(usage='usage: %prog [options] TEST-LIST-PATH')
1101 parser.add_option('-v', '--verbose', dest='verbose',
1102 action='store_true',
1103 help='Enable debug logging')
1104 (options, args) = parser.parse_args()
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001105
Jon Salz14bcbb02012-03-17 15:11:50 +08001106 if len(args) != 1:
1107 parser.error('Incorrect number of arguments')
1108
1109 factory.init_logging('ui', verbose=options.verbose)
Hung-Te Lin6bb48552012-02-09 14:37:43 +08001110 main(sys.argv[1])