blob: 51f50f3280c66801fa941a954146f3bf0ee8fd1f [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
34import subprocess
35import sys
Hung-Te Linf2f78f72012-02-08 19:27:11 +080036from itertools import izip, product
37
Hung-Te Lin6bb48552012-02-09 14:37:43 +080038# GTK and X modules
39import gobject
40import gtk
41import pango
42
43# Factory and autotest modules
Hung-Te Linf2f78f72012-02-08 19:27:11 +080044import factory_common
45from autotest_lib.client.cros import factory
46from autotest_lib.client.cros.factory import TestState
47from autotest_lib.client.cros.factory.event import Event, EventClient
48
Hung-Te Lin6bb48552012-02-09 14:37:43 +080049
Hung-Te Linf2f78f72012-02-08 19:27:11 +080050# For compatibility with tests before TestState existed
51ACTIVE = TestState.ACTIVE
52PASSED = TestState.PASSED
53FAILED = TestState.FAILED
54UNTESTED = TestState.UNTESTED
55
Hung-Te Lin6bb48552012-02-09 14:37:43 +080056# Color definition
Hung-Te Linf2f78f72012-02-08 19:27:11 +080057BLACK = gtk.gdk.Color()
58RED = gtk.gdk.Color(0xFFFF, 0, 0)
59GREEN = gtk.gdk.Color(0, 0xFFFF, 0)
60BLUE = gtk.gdk.Color(0, 0, 0xFFFF)
61WHITE = gtk.gdk.Color(0xFFFF, 0xFFFF, 0xFFFF)
Hung-Te Linf2f78f72012-02-08 19:27:11 +080062LIGHT_GREEN = gtk.gdk.color_parse('light green')
Hung-Te Linf2f78f72012-02-08 19:27:11 +080063SEP_COLOR = gtk.gdk.color_parse('grey50')
64
65RGBA_GREEN_OVERLAY = (0, 0.5, 0, 0.6)
66RGBA_YELLOW_OVERLAY = (0.6, 0.6, 0, 0.6)
67
68LABEL_COLORS = {
69 TestState.ACTIVE: gtk.gdk.color_parse('light goldenrod'),
70 TestState.PASSED: gtk.gdk.color_parse('pale green'),
71 TestState.FAILED: gtk.gdk.color_parse('tomato'),
72 TestState.UNTESTED: gtk.gdk.color_parse('dark slate grey')}
73
74LABEL_FONT = pango.FontDescription('courier new condensed 16')
Hung-Te Linbf545582012-02-15 17:08:07 +080075LABEL_LARGE_FONT = pango.FontDescription('courier new condensed 24')
Hung-Te Linf2f78f72012-02-08 19:27:11 +080076
77FAIL_TIMEOUT = 30
78
79USER_PASS_FAIL_SELECT_STR = (
80 'hit TAB to fail and ENTER to pass\n' +
81 '錯誤請按 TAB,成功請按 ENTER')
Chun-Ta Linf33efa72012-03-15 18:56:38 +080082# Resolution where original UI is designed for.
83_UI_SCREEN_WIDTH = 1280
84_UI_SCREEN_HEIGHT = 800
Hung-Te Linf2f78f72012-02-08 19:27:11 +080085
Tai-Hsu Lin606685c2012-03-14 19:10:11 +080086_LABEL_STATUS_ROW_SIZE = (300, 30)
Hung-Te Lin6bb48552012-02-09 14:37:43 +080087_LABEL_EN_SIZE = (170, 35)
88_LABEL_ZH_SIZE = (70, 35)
89_LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
90_LABEL_ZH_FONT = pango.FontDescription('normal 12')
91_LABEL_T_SIZE = (40, 35)
92_LABEL_T_FONT = pango.FontDescription('arial ultra-condensed 10')
93_LABEL_UNTESTED_FG = gtk.gdk.color_parse('grey40')
94_LABEL_TROUGH_COLOR = gtk.gdk.color_parse('grey20')
95_LABEL_STATUS_SIZE = (140, 30)
96_LABEL_STATUS_FONT = pango.FontDescription(
97 'courier new bold extra-condensed 16')
98_OTHER_LABEL_FONT = pango.FontDescription('courier new condensed 20')
99
100_ST_LABEL_EN_SIZE = (250, 35)
101_ST_LABEL_ZH_SIZE = (150, 35)
102
103_NO_ACTIVE_TEST_DELAY_MS = 500
104
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800105# ---------------------------------------------------------------------------
106# Client Library
107
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800108
Hung-Te Lin4b0b61f2012-03-05 14:20:06 +0800109# TODO(hungte) Replace gtk_lock by gtk.gdk.lock when it's availble (need pygtk
110# 2.2x, and we're now pinned by 2.1x)
111class _GtkLock(object):
112 __enter__ = gtk.gdk.threads_enter
113 def __exit__(*ignored):
114 gtk.gdk.threads_leave()
115
116
117gtk_lock = _GtkLock()
118
119
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800120def make_label(message, font=LABEL_FONT, fg=LIGHT_GREEN,
121 size=None, alignment=None):
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800122 """Returns a label widget.
123
124 A wrapper for gtk.Label. The unit of size is pixels under resolution
125 _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
126
127 @param message: A string to be displayed.
128 @param font: Font descriptor for the label.
129 @param fg: Foreground color.
130 @param size: Minimum size for this label.
131 @param alignment: Alignment setting.
132 @return: A label widget.
133 """
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800134 l = gtk.Label(message)
135 l.modify_font(font)
136 l.modify_fg(gtk.STATE_NORMAL, fg)
137 if size:
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800138 # Convert size according to the current resolution.
139 l.set_size_request(*convert_pixels(size))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800140 if alignment:
141 l.set_alignment(*alignment)
142 return l
143
144
Tai-Hsu Lin606685c2012-03-14 19:10:11 +0800145def make_status_row(init_prompt,
146 init_status,
147 label_size=_LABEL_STATUS_ROW_SIZE):
148 """Returns a widget that live updates prompt and status in a row.
149
150 Args:
151 init_prompt: The prompt label text.
152 init_status: The status label text.
153 label_size: The desired size of the prompt label and the status label.
154
155 Returns:
156 1) A dict whose content is linked by the widget.
157 2) A widget to render dict content in "prompt: status" format.
158 """
159 display_dict = {}
160 display_dict['prompt'] = init_prompt
161 display_dict['status'] = init_status
162
163 def prompt_label_expose(widget, event):
164 prompt = display_dict['prompt']
165 widget.set_text(prompt)
166
167 def status_label_expose(widget, event):
168 status = display_dict['status']
169 widget.set_text(status)
170 widget.modify_fg(gtk.STATE_NORMAL, LABEL_COLORS[status])
171
172 prompt_label = make_label(
173 init_prompt, size=label_size,
174 alignment=(0, 0.5))
175 delimiter_label = make_label(':', alignment=(0, 0.5))
176 status_label = make_label(
177 init_status, size=label_size,
178 alignment=(0, 0.5), fg=LABEL_COLORS[init_status])
179
180 widget = gtk.HBox()
181 widget.pack_end(status_label, False, False)
182 widget.pack_end(delimiter_label, False, False)
183 widget.pack_end(prompt_label, False, False)
184
185 status_label.connect('expose_event', status_label_expose)
186 prompt_label.connect('expose_event', prompt_label_expose)
187 return display_dict, widget
188
189
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800190def convert_pixels(size):
191 """Converts a pair in pixel that is suitable for current resolution.
192
193 GTK takes pixels as its unit in many function calls. To maintain the
194 consistency of the UI in different resolution, a conversion is required.
195 Take current resolution and (_UI_SCREEN_WIDTH, _UI_SCREEN_HEIGHT) as
196 the original resolution, this function returns a pair of width and height
197 that is converted for current resolution.
198
199 Because pixels in negative usually indicates unspecified, no conversion
200 will be done for negative pixels.
201
202 In addition, the aspect ratio is not maintained in this function.
203
204 Usage Example:
205 width,_ = convert_pixels((20,-1))
206
207 @param size: A pair of pixels that designed under original resolution.
208 @return: A pair of pixels of (width, height) format.
209 Pixels returned are always integer.
210 """
211 return (int(float(size[0]) / _UI_SCREEN_WIDTH * gtk.gdk.screen_width()
212 if (size[0] > 0) else size[0]),
213 int(float(size[1]) / _UI_SCREEN_HEIGHT * gtk.gdk.screen_height()
214 if (size[1] > 0) else size[1]))
215
216
217def make_hsep(height=1):
218 """Returns a widget acts as a horizontal separation line.
219
220 The unit is pixels under resolution _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
221 """
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800222 frame = gtk.EventBox()
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800223 # Convert height according to the current resolution.
224 frame.set_size_request(*convert_pixels((-1, height)))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800225 frame.modify_bg(gtk.STATE_NORMAL, SEP_COLOR)
226 return frame
227
228
229def make_vsep(width=1):
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800230 """Returns a widget acts as a vertical separation line.
231
232 The unit is pixels under resolution _UI_SCREEN_WIDTH*_UI_SCREEN_HEIGHT.
233 """
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800234 frame = gtk.EventBox()
Chun-Ta Linf33efa72012-03-15 18:56:38 +0800235 # Convert width according to the current resolution.
236 frame.set_size_request(*convert_pixels((width, -1)))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800237 frame.modify_bg(gtk.STATE_NORMAL, SEP_COLOR)
238 return frame
239
240
Hung-Te Linaf8ec542012-03-14 20:01:40 +0800241def make_countdown_widget(prompt=None, value=None, fg=LIGHT_GREEN):
242 if prompt is None:
243 prompt = 'time remaining / 剩餘時間: '
244 if value is None:
245 value = '%s' % FAIL_TIMEOUT
246 title = make_label(prompt, fg=fg, alignment=(1, 0.5))
247 countdown = make_label(value, fg=fg, alignment=(0, 0.5))
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800248 hbox = gtk.HBox()
249 hbox.pack_start(title)
250 hbox.pack_start(countdown)
251 eb = gtk.EventBox()
252 eb.modify_bg(gtk.STATE_NORMAL, BLACK)
253 eb.add(hbox)
254 return eb, countdown
255
256
257def hide_cursor(gdk_window):
258 pixmap = gtk.gdk.Pixmap(None, 1, 1, 1)
259 color = gtk.gdk.Color()
260 cursor = gtk.gdk.Cursor(pixmap, pixmap, color, color, 0, 0)
261 gdk_window.set_cursor(cursor)
262
263
264def calc_scale(wanted_x, wanted_y):
265 (widget_size_x, widget_size_y) = factory.get_shared_data('test_widget_size')
266 scale_x = (0.9 * widget_size_x) / wanted_x
267 scale_y = (0.9 * widget_size_y) / wanted_y
268 scale = scale_y if scale_y < scale_x else scale_x
269 scale = 1 if scale > 1 else scale
270 factory.log('scale: %s' % scale)
271 return scale
272
273
274def trim(text, length):
275 if len(text) > length:
276 text = text[:length-3] + '...'
277 return text
278
279
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800280class InputError(ValueError):
281 """Execption for input window callbacks to change status text message."""
282 pass
283
284
Hung-Te Linbf545582012-02-15 17:08:07 +0800285def make_input_window(prompt=None,
286 init_value=None,
287 msg_invalid=None,
288 font=None,
289 on_validate=None,
290 on_keypress=None,
291 on_complete=None):
292 """
293 Creates a widget to prompt user for a valid string.
294
295 @param prompt: A string to be displayed. None for default message.
296 @param init_value: Initial value to be set.
297 @param msg_invalid: Status string to display when input is invalid. None for
298 default message.
299 @param font: Font specification (string or pango.FontDescription) for label
300 and entry. None for default large font.
301 @param on_validate: A callback function to validate if the input from user
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800302 is valid. None for allowing any non-empty input. Any ValueError or
303 ui.InputError raised during execution in on_validate will be displayed
304 in bottom status.
Hung-Te Linbf545582012-02-15 17:08:07 +0800305 @param on_keypress: A callback function when each keystroke is hit.
306 @param on_complete: A callback function when a valid string is passed.
307 None to stop (gtk.main_quit).
308 @return: A widget with prompt, input entry, and status label.
Chun-ta Lin2e7f44c2012-03-03 07:29:36 +0800309 In addition, a method called get_entry() is added to the widget to
310 provide controls on the entry.
Hung-Te Linbf545582012-02-15 17:08:07 +0800311 """
312 DEFAULT_MSG_INVALID = "Invalid input / 輸入不正確"
313 DEFAULT_PROMPT = "Enter Data / 輸入資料:"
314
315 def enter_callback(entry):
316 text = entry.get_text()
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800317 try:
318 if (on_validate and (not on_validate(text))) or (not text.strip()):
319 raise ValueError(msg_invalid)
Hung-Te Linbf545582012-02-15 17:08:07 +0800320 on_complete(text) if on_complete else gtk.main_quit()
Hung-Te Lin6dc799c2012-03-13 22:10:47 +0800321 except ValueError as e:
322 gtk.gdk.beep()
323 status_label.set_text('ERROR: %s' % e.message)
Hung-Te Linbf545582012-02-15 17:08:07 +0800324 return True
325
326 def key_press_callback(entry, key):
327 status_label.set_text('')
328 if on_keypress:
329 return on_keypress(entry, key)
330 return False
331
332 # Populate default parameters
333 if msg_invalid is None:
334 msg_invalid = DEFAULT_MSG_INVALID
335
336 if prompt is None:
337 prompt = DEFAULT_PROMPT
338
339 if font is None:
340 font = LABEL_LARGE_FONT
341 elif not isinstance(font, pango.FontDescription):
342 font = pango.FontDescription(font)
343
344 widget = gtk.VBox()
345 label = make_label(prompt, font=font)
346 status_label = make_label('', font=font)
347 entry = gtk.Entry()
348 entry.modify_font(font)
349 entry.connect("activate", enter_callback)
350 entry.connect("key_press_event", key_press_callback)
351 if init_value:
352 entry.set_text(init_value)
353 widget.modify_bg(gtk.STATE_NORMAL, BLACK)
354 status_label.modify_fg(gtk.STATE_NORMAL, RED)
355 widget.add(label)
356 widget.pack_start(entry)
357 widget.pack_start(status_label)
Chun-ta Lin2e7f44c2012-03-03 07:29:36 +0800358
359 # Method for getting the entry.
360 widget.get_entry = lambda : entry
Hung-Te Linbf545582012-02-15 17:08:07 +0800361 return widget
362
363
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800364def make_summary_box(tests, state_map, rows=15):
365 '''
366 Creates a widget display status of a set of test.
367
368 @param tests: A list of FactoryTest nodes whose status (and children's
369 status) should be displayed.
370 @param state_map: The state map as provide by the state instance.
371 @param rows: The number of rows to display.
372 @return: A tuple (widget, label_map), where widget is the widget, and
373 label_map is a map from each test to the corresponding label.
374 '''
375 LABEL_EN_SIZE = (170, 35)
376 LABEL_EN_SIZE_2 = (450, 25)
377 LABEL_EN_FONT = pango.FontDescription('courier new extra-condensed 16')
378
379 all_tests = sum([list(t.walk(in_order=True)) for t in tests], [])
380 columns = len(all_tests) / rows + (len(all_tests) % rows != 0)
381
382 info_box = gtk.HBox()
383 info_box.set_spacing(20)
384 for status in (TestState.ACTIVE, TestState.PASSED,
385 TestState.FAILED, TestState.UNTESTED):
386 label = make_label(status,
387 size=LABEL_EN_SIZE,
388 font=LABEL_EN_FONT,
389 alignment=(0.5, 0.5),
390 fg=LABEL_COLORS[status])
391 info_box.pack_start(label, False, False)
392
393 vbox = gtk.VBox()
394 vbox.set_spacing(20)
395 vbox.pack_start(info_box, False, False)
396
397 label_map = {}
398
399 if all_tests:
400 status_table = gtk.Table(rows, columns, True)
401 for (j, i), t in izip(product(xrange(columns), xrange(rows)),
402 all_tests):
403 msg_en = ' ' * (t.depth() - 1) + t.label_en
404 msg_en = trim(msg_en, 12)
405 if t.label_zh:
406 msg = '{0:<12} ({1})'.format(msg_en, t.label_zh)
407 else:
408 msg = msg_en
409 status = state_map[t].status
410 status_label = make_label(msg,
411 size=LABEL_EN_SIZE_2,
412 font=LABEL_EN_FONT,
413 alignment=(0.0, 0.5),
414 fg=LABEL_COLORS[status])
415 label_map[t] = status_label
416 status_table.attach(status_label, j, j+1, i, i+1)
417 vbox.pack_start(status_table, False, False)
418
419 return vbox, label_map
420
421
422def run_test_widget(dummy_job, test_widget,
423 invisible_cursor=True,
424 window_registration_callback=None,
425 cleanup_callback=None):
426 test_widget_size = factory.get_shared_data('test_widget_size')
427
428 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
429 window.modify_bg(gtk.STATE_NORMAL, BLACK)
430 window.set_size_request(*test_widget_size)
431
432 def show_window():
433 window.show()
434 window.window.raise_() # pylint: disable=E1101
435 gtk.gdk.pointer_grab(window.window, confine_to=window.window)
436 if invisible_cursor:
437 hide_cursor(window.window)
438
439 test_path = factory.get_current_test_path()
440
441 def handle_event(event):
442 if (event.type == Event.Type.STATE_CHANGE and
443 test_path and event.path == test_path and
444 event.state.visible):
445 show_window()
446
447 event_client = EventClient(
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800448 callback=handle_event, event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
Hung-Te Linf2f78f72012-02-08 19:27:11 +0800449
450 align = gtk.Alignment(xalign=0.5, yalign=0.5)
451 align.add(test_widget)
452
453 window.add(align)
454 for c in window.get_children():
455 # Show all children, but not the window itself yet.
456 c.show_all()
457
458 if window_registration_callback is not None:
459 window_registration_callback(window)
460
461 # Show the window if it is the visible test, or if the test_path is not
462 # available (e.g., run directly from the command line).
463 if (not test_path) or (
464 TestState.from_dict_or_object(
465 factory.get_state_instance().get_test_state(test_path)).visible):
466 show_window()
467 else:
468 window.hide()
469
470 gtk.main()
471
472 gtk.gdk.pointer_ungrab()
473
474 if cleanup_callback is not None:
475 cleanup_callback()
476
477 del event_client
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800478
479
480# ---------------------------------------------------------------------------
481# Server Implementation
482
483
484class Console(object):
485 '''Display a progress log. Implemented by launching an borderless
486 xterm at a strategic location, and running tail against the log.'''
487
488 def __init__(self, allocation):
489 xterm_coords = '145x13+%d+%d' % (allocation.x, allocation.y)
490 logging.info('xterm_coords = %s', xterm_coords)
491 xterm_opts = ('-bg black -fg lightgray -bw 0 -g %s' % xterm_coords)
492 xterm_cmd = (('urxvt %s -e bash -c ' % xterm_opts).split() +
493 ['tail -f "%s"' % factory.CONSOLE_LOG_PATH])
494 logging.info('xterm_cmd = %s', xterm_cmd)
495 self._proc = subprocess.Popen(xterm_cmd)
496
497 def __del__(self):
498 logging.info('console_proc __del__')
499 self._proc.kill()
500
501
502class TestLabelBox(gtk.EventBox): # pylint: disable=R0904
503
504 def __init__(self, test, show_shortcut=False):
505 gtk.EventBox.__init__(self)
506 self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[TestState.UNTESTED])
507
508 label_en = make_label(test.label_en, size=_LABEL_EN_SIZE,
509 font=_LABEL_EN_FONT, alignment=(0.5, 0.5),
510 fg=_LABEL_UNTESTED_FG)
511 label_zh = make_label(test.label_zh, size=_LABEL_ZH_SIZE,
512 font=_LABEL_ZH_FONT, alignment=(0.5, 0.5),
513 fg=_LABEL_UNTESTED_FG)
514 label_t = make_label('C-' + test.kbd_shortcut.upper(),
515 size=_LABEL_T_SIZE, font=_LABEL_T_FONT,
516 alignment=(0.5, 0.5), fg=BLACK)
517
518 # build a better label_en with shortcuts
519 index_hotkey = test.label_en.upper().find(test.kbd_shortcut.upper())
520 if show_shortcut and index_hotkey >= 0:
521 attrs = label_en.get_attributes() or pango.AttrList()
522 attrs.insert(pango.AttrUnderline(
523 pango.UNDERLINE_LOW, index_hotkey, index_hotkey + 1))
524 attrs.insert(pango.AttrWeight(
525 pango.WEIGHT_BOLD, index_hotkey, index_hotkey + 1))
526 label_en.set_attributes(attrs)
527
528 hbox = gtk.HBox()
529 hbox.pack_start(label_en, False, False)
530 hbox.pack_start(label_zh, False, False)
531 hbox.pack_start(label_t, False, False)
532 self.add(hbox)
533 self.label_list = [label_en, label_zh]
534
535 def update(self, state):
536 label_fg = (_LABEL_UNTESTED_FG if state.status == TestState.UNTESTED
537 else BLACK)
538 for label in self.label_list:
539 label.modify_fg(gtk.STATE_NORMAL, label_fg)
540 self.modify_bg(gtk.STATE_NORMAL, LABEL_COLORS[state.status])
541 self.queue_draw()
542
543
544class UiState(object):
545
546 def __init__(self, test_widget_box):
547 self._test_widget_box = test_widget_box
548 self._label_box_map = {}
549 self._transition_count = 0
550
551 self._active_test_label_map = None
552
553 def _remove_state_widget(self):
554 """Remove any existing state widgets."""
555 for child in self._test_widget_box.get_children():
556 self._test_widget_box.remove(child)
557 self._active_test_label_map = None
558
559 def update_test_label(self, test, state):
560 label_box = self._label_box_map.get(test)
561 if label_box:
562 label_box.update(state)
563
564 def update_test_state(self, test_list, state_map):
565 active_tests = [
566 t for t in test_list.walk()
567 if t.is_leaf() and state_map[t].status == TestState.ACTIVE]
568 has_active_ui = any(t.has_ui for t in active_tests)
569
570 if not active_tests:
571 # Display the "no active tests" widget if there are still no
572 # active tests after _NO_ACTIVE_TEST_DELAY_MS.
573 def run(transition_count):
574 if transition_count != self._transition_count:
575 # Something has happened
576 return False
577
578 self._transition_count += 1
579 self._remove_state_widget()
580
581 self._test_widget_box.set(0.5, 0.5, 0.0, 0.0)
582 self._test_widget_box.set_padding(0, 0, 0, 0)
583 label_box = gtk.EventBox()
584 label_box.modify_bg(gtk.STATE_NORMAL, BLACK)
585 label = make_label('no active test', font=_OTHER_LABEL_FONT,
586 alignment=(0.5, 0.5))
587 label_box.add(label)
588 self._test_widget_box.add(label_box)
589 self._test_widget_box.show_all()
590
591 gobject.timeout_add(_NO_ACTIVE_TEST_DELAY_MS, run,
592 self._transition_count)
593 return
594
595 self._transition_count += 1
596
597 if has_active_ui:
598 # Remove the widget (if any) since there is an active test
599 # with a UI.
600 self._remove_state_widget()
601 return
602
603 if (self._active_test_label_map is not None and
604 all(t in self._active_test_label_map for t in active_tests)):
605 # All active tests are already present in the summary, so just
606 # update their states.
607 for test, label in self._active_test_label_map.iteritems():
608 label.modify_fg(
609 gtk.STATE_NORMAL,
610 LABEL_COLORS[state_map[test].status])
611 return
612
613 self._remove_state_widget()
614 # No active UI; draw summary of current test states
615 self._test_widget_box.set(0.5, 0.0, 0.0, 0.0)
616 self._test_widget_box.set_padding(40, 0, 0, 0)
617 vbox, self._active_test_label_map = make_summary_box(
618 [t for t in test_list.subtests
619 if state_map[t].status == TestState.ACTIVE],
620 state_map)
621 self._test_widget_box.add(vbox)
622 self._test_widget_box.show_all()
623
624 def set_label_box(self, test, label_box):
625 self._label_box_map[test] = label_box
626
627
628def main(test_list_path):
629 '''Starts the main UI.
630
631 This is launched by the autotest/cros/factory/client.
632 When operators press keyboard shortcuts, the shortcut
633 value is sent as an event to the control program.'''
634
635 test_list = None
636 ui_state = None
637 event_client = None
638
639 # Delay loading Xlib because Xlib is currently not available in image build
640 # process host-depends list, and it's only required by the main UI, not all
641 # the tests using UI library (in other words, it'll be slower and break the
642 # build system if Xlib is globally imported).
643 try:
644 from Xlib import X
645 from Xlib.display import Display
646 disp = Display()
647 except:
648 logging.error('Failed loading X modules')
649 raise
650
651 def handle_key_release_event(_, event):
652 logging.info('base ui key event (%s)', event.keyval)
653 return True
654
655 def handle_event(event):
656 if event.type == Event.Type.STATE_CHANGE:
657 test = test_list.lookup_path(event.path)
658 state_map = test_list.get_state_map()
659 ui_state.update_test_label(test, state_map[test])
660 ui_state.update_test_state(test_list, state_map)
661
662 def grab_shortcut_keys(kbd_shortcuts):
663 root = disp.screen().root
664 keycode_map = {}
665
666 def handle_xevent( # pylint: disable=W0102
667 dummy_src, dummy_cond, xhandle=root.display,
668 keycode_map=keycode_map):
669 for dummy_i in range(0, xhandle.pending_events()):
670 xevent = xhandle.next_event()
671 if xevent.type == X.KeyPress:
672 keycode = xevent.detail
Jon Salz28a60912012-03-13 13:29:13 +0800673 if keycode in keycode_map:
674 event_client.post_event(Event('kbd_shortcut',
675 key=keycode_map[keycode]))
676 else:
677 logging.warning('Unbound keycode %s' % keycode)
Hung-Te Lin6bb48552012-02-09 14:37:43 +0800678 return True
679
680 # We want to receive KeyPress events
681 root.change_attributes(event_mask = X.KeyPressMask)
682
683 for mod, shortcut in ([(X.ControlMask, k) for k in kbd_shortcuts] +
684 [(X.Mod1Mask, 'Tab')]): # Mod1 = Alt
685 keysym = gtk.gdk.keyval_from_name(shortcut)
686 keycode = disp.keysym_to_keycode(keysym)
687 keycode_map[keycode] = shortcut
688 root.grab_key(keycode, mod, 1,
689 X.GrabModeAsync, X.GrabModeAsync)
690
691 # This flushes the XGrabKey calls to the server.
692 for dummy_x in range(0, root.display.pending_events()):
693 root.display.next_event()
694 gobject.io_add_watch(root.display, gobject.IO_IN, handle_xevent)
695
696
697 test_list = factory.read_test_list(test_list_path)
698
699 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
700 window.connect('destroy', lambda _: gtk.main_quit())
701 window.modify_bg(gtk.STATE_NORMAL, BLACK)
702
703 event_client = EventClient(
704 callback=handle_event,
705 event_loop=EventClient.EVENT_LOOP_GOBJECT_IO)
706
707 screen = window.get_screen()
708 if (screen is None):
709 logging.info('ERROR: communication with the X server is not working, ' +
710 'could not find a working screen. UI exiting.')
711 sys.exit(1)
712
713 screen_size_str = os.environ.get('CROS_SCREEN_SIZE')
714 if screen_size_str:
715 match = re.match(r'^(\d+)x(\d+)$', screen_size_str)
716 assert match, 'CROS_SCREEN_SIZE should be {width}x{height}'
717 screen_size = (int(match.group(1)), int(match.group(2)))
718 else:
719 screen_size = (screen.get_width(), screen.get_height())
720 window.set_size_request(*screen_size)
721
722 label_trough = gtk.VBox()
723 label_trough.set_spacing(0)
724
725 rhs_box = gtk.EventBox()
726 rhs_box.modify_bg(gtk.STATE_NORMAL, _LABEL_TROUGH_COLOR)
727 rhs_box.add(label_trough)
728
729 console_box = gtk.EventBox()
730 console_box.set_size_request(-1, 180)
731 console_box.modify_bg(gtk.STATE_NORMAL, BLACK)
732
733 test_widget_box = gtk.Alignment()
734 test_widget_box.set_size_request(-1, -1)
735
736 lhs_box = gtk.VBox()
737 lhs_box.pack_end(console_box, False, False)
738 lhs_box.pack_start(test_widget_box)
739 lhs_box.pack_start(make_hsep(3), False, False)
740
741 base_box = gtk.HBox()
742 base_box.pack_end(rhs_box, False, False)
743 base_box.pack_end(make_vsep(3), False, False)
744 base_box.pack_start(lhs_box)
745
746 window.connect('key-release-event', handle_key_release_event)
747 window.add_events(gtk.gdk.KEY_RELEASE_MASK)
748
749 ui_state = UiState(test_widget_box)
750
751 for test in test_list.subtests:
752 label_box = TestLabelBox(test, True)
753 ui_state.set_label_box(test, label_box)
754 label_trough.pack_start(label_box, False, False)
755 label_trough.pack_start(make_hsep(), False, False)
756
757 window.add(base_box)
758 window.show_all()
759
760 state_map = test_list.get_state_map()
761 for test, state in test_list.get_state_map().iteritems():
762 ui_state.update_test_label(test, state)
763 ui_state.update_test_state(test_list, state_map)
764
765 grab_shortcut_keys(test_list.kbd_shortcut_map.keys())
766
767 hide_cursor(window.window)
768
769 test_widget_allocation = test_widget_box.get_allocation()
770 test_widget_size = (test_widget_allocation.width,
771 test_widget_allocation.height)
772 factory.set_shared_data('test_widget_size', test_widget_size)
773
774 dummy_console = Console(console_box.get_allocation())
775
776 event_client.post_event(Event(Event.Type.UI_READY))
777
778 logging.info('cros/factory/ui setup done, starting gtk.main()...')
779 gtk.main()
780 logging.info('cros/factory/ui gtk.main() finished, exiting.')
781
782
783if __name__ == '__main__':
784 if len(sys.argv) != 2:
785 print 'usage: %s <test list path>' % sys.argv[0]
786 sys.exit(1)
787
788 factory.init_logging("cros/factory/ui", verbose=True)
789 main(sys.argv[1])