• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python
2# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""This module sets up the system for the touch device firmware test suite."""
7
8import getopt
9import glob
10import logging
11import os
12import sys
13
14import common
15import cros_gs
16import firmware_utils
17
18# TODO(josephsih): remove this hack when not relying on pygtk.
19# The pygtk related stuffs are needed by firmware_window below.
20if not firmware_utils.install_pygtk():
21    sys.exit(1)
22
23import firmware_window
24import keyboard_device
25import mtb
26import test_conf as conf
27import test_flow
28import touch_device
29import validators
30
31from common_util import print_and_exit
32from firmware_constants import MODE, OPTIONS
33from report_html import ReportHtml
34
35
36def _display_test_result(report_html_name, flag_skip_html):
37    """Display the test result html doc using telemetry."""
38    if not flag_skip_html and os.path.isdir('/usr/local/telemetry'):
39        import chrome
40
41        base_url = os.path.basename(report_html_name)
42        url = os.path.join('file://' + conf.docroot, base_url)
43        logging.info('Navigate to the URL: %s', url)
44
45        # Launch a browser to display the url.
46        print 'Display the html test report on the browser.'
47        print 'This may take a while...\n'
48        chrome.Chrome().browser.tabs[0].Navigate(url)
49    else:
50        print 'You can look up the html test result in %s' % report_html_name
51
52
53class firmware_TouchMTB:
54    """Set up the system for touch device firmware tests."""
55
56    def __init__(self, options):
57        self.options = options
58
59        self.test_version = 'test_' + self._get_test_version()
60
61        # Get the board name
62        self._get_board()
63
64        # We may need to use a device description file to create a fake device
65        # for replay purpose.
66        self._get_device_description_file()
67
68        # Create the touch device
69        # If you are going to be testing a touchscreen, set it here
70        self.touch_device = touch_device.TouchDevice(
71            is_touchscreen=options[OPTIONS.TOUCHSCREEN],
72            device_description_file=self.device_description_file)
73        self._check_device(self.touch_device)
74        validators.init_base_validator(self.touch_device)
75
76        # Create the keyboard device.
77        self.keyboard = keyboard_device.KeyboardDevice()
78        self._check_device(self.keyboard)
79
80        # Get the MTB parser.
81        self.parser = mtb.MtbParser()
82
83        # Create a simple gtk window.
84        self._get_screen_size()
85        self._get_touch_device_window_geometry()
86        self._get_prompt_frame_geometry()
87        self._get_result_frame_geometry()
88        self.win = firmware_window.FirmwareWindow(
89                size=self.screen_size,
90                prompt_size=self.prompt_frame_size,
91                image_size=self.touch_device_window_size,
92                result_size=self.result_frame_size)
93
94        mode = options[OPTIONS.MODE]
95        if options[OPTIONS.RESUME]:
96            # Use the firmware version of the real touch device for recording.
97            firmware_version = self.touch_device.get_firmware_version()
98            self.log_dir = options[OPTIONS.RESUME]
99        elif options[OPTIONS.REPLAY]:
100            # Use the firmware version of the specified logs for replay.
101            self.log_dir = options[OPTIONS.REPLAY]
102            fw_str, date = firmware_utils.get_fw_and_date(self.log_dir)
103            _, firmware_version = fw_str.split(conf.fw_prefix)
104        else:
105            # Use the firmware version of the real touch device for recording.
106            firmware_version = self.touch_device.get_firmware_version()
107            self.log_dir = firmware_utils.create_log_dir(firmware_version, mode)
108
109        # Save the device description file for future replay purpose if needed.
110        if not (self.options[OPTIONS.REPLAY] or self.options[OPTIONS.RESUME]):
111            self._save_device_description_file()
112
113        # Create the HTML report object and the output object to print messages
114        # on the window and to print the results in the report.
115        self._create_report_name(mode, firmware_version)
116        self.report_html = ReportHtml(self.report_html_name,
117                                      self.screen_size,
118                                      self.touch_device_window_size,
119                                      conf.score_colors,
120                                      self.test_version)
121        self.output = firmware_utils.Output(self.log_dir,
122                                            self.report_name,
123                                            self.win, self.report_html)
124
125        # Get the test_flow object which will guide through the gesture list.
126        self.test_flow = test_flow.TestFlow(self.touch_device_window_geometry,
127                                            self.touch_device,
128                                            self.keyboard,
129                                            self.win,
130                                            self.parser,
131                                            self.output,
132                                            self.test_version,
133                                            self.board,
134                                            firmware_version,
135                                            options)
136
137        # Register some callback functions for firmware window
138        self.win.register_callback('expose_event',
139                                   self.test_flow.init_gesture_setup_callback)
140
141        # Register a callback function to watch keyboard input events.
142        # This is required because the set_input_focus function of a window
143        # is flaky maybe due to problems of the window manager.
144        # Hence, we handle the keyboard input at a lower level.
145        self.win.register_io_add_watch(self.test_flow.user_choice_callback,
146                                       self.keyboard.system_device)
147
148        # Stop power management so that the screen does not dim during tests
149        firmware_utils.stop_power_management()
150
151    def _check_device(self, device):
152        """Check if a device has been created successfully."""
153        if not device.exists():
154            logging.error('Cannot find device_node.')
155            exit(1)
156
157    def _get_test_version(self):
158        """Get the test suite version number."""
159        if not os.path.isfile(conf.version_filename):
160            err_msg = ('Error: cannot find the test version file: %s\n\n'
161                       'Perform the following steps in chroot to install '
162                       'the test suite correctly:\n'
163                       'Step 1: (cr) $ cd ~/trunk/src/scripts\n'
164                       'Step 2: (cr) $ test_that --autotest_dir '
165                       '~/trunk/src/third_party/autotest/files '
166                       '$MACHINE_IP firmware_TouchMTBSetup\n')
167            print err_msg % conf.version_filename
168            sys.exit(1)
169
170        with open(conf.version_filename) as version_file:
171            return version_file.read()
172
173    def _get_board(self):
174        """Get the board.
175
176        If this is in replay mode, get the board from the replay directory.
177        Otherwise, get the board name from current chromebook machine.
178        """
179        replay_dir = self.options[OPTIONS.REPLAY]
180        if replay_dir:
181            self.board = firmware_utils.get_board_from_directory(replay_dir)
182            if self.board is None:
183                msg = 'Error: cannot get the board from the replay directory %s'
184                print_and_exit(msg % replay_dir)
185        else:
186            self.board = firmware_utils.get_board()
187        print '      board: %s' % self.board
188
189    def _get_device_ext(self):
190        """Set the file extension of the device description filename to
191        'touchscreen' if it is a touchscreen; otherwise, set it to 'touchpad'.
192        """
193        return ('touchscreen' if self.options[OPTIONS.TOUCHSCREEN] else
194                'touchpad')
195
196    def _get_device_description_file(self):
197        """Get the device description file for replay purpose.
198
199        Get the device description file only when it is in replay mode and
200        the system DEVICE option is not specified.
201
202        The priority to locate the device description file:
203        (1) in the directory specified by the REPLAY option,
204        (2) in the tests/device/ directory
205
206        A device description file name looks like "link.touchpad"
207        """
208        self.device_description_file = None
209        # Replay without using the system device. So use a mocked device.
210        if self.options[OPTIONS.REPLAY] and not self.options[OPTIONS.DEVICE]:
211            device_ext = self._get_device_ext()
212            board = self.board
213            descriptions = [
214                # (1) Try to find the device description in REPLAY directory.
215                (self.options[OPTIONS.REPLAY], '*.%s' % device_ext),
216                # (2) Try to find the device description in tests/device/
217                (conf.device_description_dir, '%s.%s' % (board, device_ext),)
218            ]
219
220            for description_dir, description_pattern in descriptions:
221                files = glob.glob(os.path.join(description_dir,
222                                               description_pattern))
223                if files:
224                    self.device_description_file = files[0]
225                    break
226            else:
227                msg = 'Error: cannot find the device description file.'
228                print_and_exit(msg)
229        print '      device description file: %s' % self.device_description_file
230
231    def _save_device_description_file(self):
232        """Save the device description file for future replay."""
233        filename = '%s.%s' % (self.board, self._get_device_ext())
234        filepath = os.path.join(self.log_dir, filename)
235        if not self.touch_device.save_device_description_file(
236                filepath, self.board):
237            msg = 'Error: fail to save the device description file: %s'
238            print_and_exit(msg % filepath)
239
240    def _create_report_name(self, mode, firmware_version):
241        """Create the report names for both plain-text and html files.
242
243        A typical html file name looks like:
244            touch_firmware_report-lumpy-fw_11.25-20121016_080924.html
245        """
246        firmware_str = conf.fw_prefix + firmware_version
247        curr_time = firmware_utils.get_current_time_str()
248        fname = conf.filename.sep.join([conf.report_basename,
249                                        self.board,
250                                        firmware_str,
251                                        mode,
252                                        curr_time])
253        self.report_name = os.path.join(self.log_dir, fname)
254        self.report_html_name = self.report_name + conf.html_ext
255
256    def _get_screen_size(self):
257        """Get the screen size."""
258        self.screen_size = firmware_utils.get_screen_size()
259
260    def _get_touch_device_window_geometry(self):
261        """Get the preferred window geometry to display mtplot."""
262        display_ratio = 0.7
263        self.touch_device_window_geometry = \
264                self.touch_device.get_display_geometry(
265                self.screen_size, display_ratio)
266        self.touch_device_window_size = self.touch_device_window_geometry[0:2]
267
268    def _get_prompt_frame_geometry(self):
269        """Get the display geometry of the prompt frame."""
270        (_, wint_height, _, _) = self.touch_device_window_geometry
271        screen_width, screen_height = self.screen_size
272        win_x = 0
273        win_y = 0
274        win_width = screen_width
275        win_height = screen_height - wint_height
276        self.winp_geometry = (win_x, win_y, win_width, win_height)
277        self.prompt_frame_size = (win_width, win_height)
278
279    def _get_result_frame_geometry(self):
280        """Get the display geometry of the test result frame."""
281        (wint_width, wint_height, _, _) = self.touch_device_window_geometry
282        screen_width, _ = self.screen_size
283        win_width = screen_width - wint_width
284        win_height = wint_height
285        self.result_frame_size = (win_width, win_height)
286
287    def main(self):
288        """A helper to enter gtk main loop."""
289        # Enter the window event driven mode.
290        fw.win.main()
291
292        # Resume the power management.
293        firmware_utils.start_power_management()
294
295        flag_skip_html = self.options[OPTIONS.SKIP_HTML]
296        try:
297            _display_test_result(self.report_html_name, flag_skip_html)
298        except Exception, e:
299            print 'Warning: cannot display the html result file: %s\n' % e
300            print ('You can access the html result file: "%s"\n' %
301                   self.report_html_name)
302        finally:
303            print 'You can upload all data in the latest result directory:'
304            print '  $ DISPLAY=:0 OPTIONS="-u latest" python main.py\n'
305            print ('You can also upload any test result directory, e.g., '
306                   '"20130702_063631-fw_1.23-manual", in "%s"' %
307                   conf.log_root_dir)
308            print ('  $ DISPLAY=:0 OPTIONS="-u 20130702_063631-fw_11.23-manual"'
309                   ' python main.py\n')
310
311            if self.options[OPTIONS.MODE] == MODE.NOISE:
312                print ('You can generate a summary of the extended noise test_flow '
313                       'by copying the html report to your computer and running '
314                       'noise_summary.py, located in '
315                       '~/trunk/src/third_party/autotest/files/client/site_tests/firmware_TouchMTB/')
316
317
318def upload_to_gs(log_dir):
319    """Upload the gesture event files specified in log_dir to Google cloud
320    storage server.
321
322    @param log_dir: the log directory of which the gesture event files are
323            to be uploaded to Google cloud storage server
324    """
325    # Set up gsutil package.
326    # The board argument is used to locate the proper bucket directory
327    gs = cros_gs.CrosGs(firmware_utils.get_board())
328
329    log_path = os.path.join(conf.log_root_dir, log_dir)
330    if not os.path.isdir(log_path):
331        print_and_exit('Error: the log path "%s" does not exist.' % log_path)
332
333    print 'Uploading "%s" to %s ...\n' % (log_path, gs.bucket)
334    try:
335        gs.upload(log_path)
336    except Exception, e:
337        msg = 'Error in uploading event files in %s: %s.'
338        print_and_exit(msg % (log_path, e))
339
340
341def _usage_and_exit():
342    """Print the usage of this program."""
343    print 'Usage: $ DISPLAY=:0 [OPTIONS="options"] python %s\n' % sys.argv[0]
344    print 'options:'
345    print '  -d, --%s' % OPTIONS.DEVICE
346    print '        use the system device for replay'
347    print '  -h, --%s' % OPTIONS.HELP
348    print '        show this help'
349    print '  -i, --%s iterations' % OPTIONS.ITERATIONS
350    print '        specify the number of iterations'
351    print '  -f, --%s' % OPTIONS.FNGENERATOR
352    print '        Indicate that (despite not having a touchbot) there is a'
353    print '        function generator attached for the noise tests'
354    print '  -m, --%s mode' % OPTIONS.MODE
355    print '        specify the gesture playing mode'
356    print '        mode could be one of the following options'
357    print '            calibration: conducting pressure calibration'
358    print '            complete: all gestures including those in ' \
359                                'both manual mode and robot mode'
360    print '            manual: all gestures minus gestures in robot mode'
361    print '            noise: an extensive, 4 hour noise test'
362    print '            robot: using robot to perform gestures automatically'
363    print '            robot_sim: robot simulation, for developer only'
364    print '  --%s log_dir' % OPTIONS.REPLAY
365    print '        Replay the gesture files and get the test results.'
366    print '        log_dir is a log sub-directory in %s' % conf.log_root_dir
367    print '  --%s log_dir' % OPTIONS.RESUME
368    print '        Resume recording the gestures files in the log_dir.'
369    print '        log_dir is a log sub-directory in %s' % conf.log_root_dir
370    print '  -s, --%s' % OPTIONS.SIMPLIFIED
371    print '        Use one variation per gesture'
372    print '  --%s' % OPTIONS.SKIP_HTML
373    print '        Do not show the html test result.'
374    print '  -t, --%s' % OPTIONS.TOUCHSCREEN
375    print '        Use the touchscreen instead of a touchpad'
376    print '  -u, --%s log_dir' % OPTIONS.UPLOAD
377    print '        Upload the gesture event files in the specified log_dir '
378    print '        to Google cloud storage server.'
379    print '        It uploads results that you already have from a previous run'
380    print '        without re-running the test.'
381    print '        log_dir could be either '
382    print '        (1) a directory in %s' % conf.log_root_dir
383    print '        (2) a full path, or'
384    print '        (3) the default "latest" directory in %s if omitted' % \
385                   conf.log_root_dir
386    print
387    print 'Example:'
388    print '  # Use the robot to perform 3 iterations of the robot gestures.'
389    print '  $ DISPLAY=:0 OPTIONS="-m robot_sim -i 3" python main.py\n'
390    print '  # Perform 1 iteration of the manual gestures.'
391    print '  $ DISPLAY=:0 OPTIONS="-m manual" python main.py\n'
392    print '  # Perform 1 iteration of all manual and robot gestures.'
393    print '  $ DISPLAY=:0 OPTIONS="-m complete" python main.py\n'
394    print '  # Perform pressure calibration.'
395    print '  $ DISPLAY=:0 OPTIONS="-m calibration" python main.py\n'
396    print '  # Use the robot to perform a latency test with Quickstep'
397    print '  $ DISPLAY=:0 OPTIONS="-m quickstep" python main.py\n'
398    print '  # Use the robot to perform an extensive, 4 hour noise test'
399    print '  $ DISPLAY=:0 OPTIONS="-m noise" python main.py\n'
400    print '  # Replay the gesture files in the latest log directory.'
401    print '  $ DISPLAY=:0 OPTIONS="--replay latest" python main.py\n'
402    example_log_dir = '20130226_040802-fw_1.2-manual'
403    print ('  # Replay the gesture files in %s/%s with a mocked device.' %
404            (conf.log_root_dir, example_log_dir))
405    print '  $ DISPLAY=:0 OPTIONS="--replay %s" python main.py\n' % \
406            example_log_dir
407    print ('  # Replay the gesture files in %s/%s with the system device.' %
408            (conf.log_root_dir, example_log_dir))
409    print ('  $ DISPLAY=:0 OPTIONS="--replay %s -d" python main.py\n' %
410            example_log_dir)
411    print '  # Resume recording the gestures in the latest log directory.'
412    print '  $ DISPLAY=:0 OPTIONS="--resume latest" python main.py\n'
413    print '  # Resume recording the gestures in %s/%s.' % (conf.log_root_dir,
414                                                           example_log_dir)
415    print '  $ DISPLAY=:0 OPTIONS="--resume %s" python main.py\n' % \
416            example_log_dir
417    print ('  # Upload the gesture event files specified in the log_dir '
418             'to Google cloud storage server.')
419    print ('  $ DISPLAY=:0 OPTIONS="-u 20130701_020120-fw_11.23-complete" '
420           'python main.py\n')
421    print ('  # Upload the gesture event files in the "latest" directory '
422           'to Google cloud storage server.')
423    print '  $ DISPLAY=:0 OPTIONS="-u latest" python main.py\n'
424
425    sys.exit(1)
426
427
428def _parsing_error(msg):
429    """Print the usage and exit when encountering parsing error."""
430    print 'Error: %s' % msg
431    _usage_and_exit()
432
433
434def _parse_options():
435    """Parse the options.
436
437    Note that the options are specified with environment variable OPTIONS,
438    because pyauto seems not compatible with command line options.
439    """
440    # Set the default values of options.
441    options = {OPTIONS.DEVICE: False,
442               OPTIONS.FNGENERATOR: False,
443               OPTIONS.ITERATIONS: 1,
444               OPTIONS.MODE: MODE.MANUAL,
445               OPTIONS.REPLAY: None,
446               OPTIONS.RESUME: None,
447               OPTIONS.SIMPLIFIED: False,
448               OPTIONS.SKIP_HTML: False,
449               OPTIONS.TOUCHSCREEN: False,
450               OPTIONS.UPLOAD: None,
451    }
452
453    # Get the command line options or get the options from environment OPTIONS
454    options_list = sys.argv[1:] or os.environ.get('OPTIONS', '').split()
455    if not options_list:
456        return options
457
458    short_opt = 'dfhi:m:stu:'
459    long_opt = [OPTIONS.DEVICE,
460                OPTIONS.FNGENERATOR,
461                OPTIONS.HELP,
462                OPTIONS.ITERATIONS + '=',
463                OPTIONS.MODE + '=',
464                OPTIONS.REPLAY + '=',
465                OPTIONS.RESUME + '=',
466                OPTIONS.SIMPLIFIED,
467                OPTIONS.SKIP_HTML,
468                OPTIONS.TOUCHSCREEN,
469                OPTIONS.UPLOAD + '=',
470    ]
471    try:
472        opts, args = getopt.getopt(options_list, short_opt, long_opt)
473    except getopt.GetoptError, err:
474        _parsing_error(str(err))
475
476    for opt, arg in opts:
477        if opt in ('-d', '--%s' % OPTIONS.DEVICE):
478            options[OPTIONS.DEVICE] = True
479        if opt in ('-f', '--%s' % OPTIONS.FNGENERATOR):
480            options[OPTIONS.FNGENERATOR] = True
481        elif opt in ('-h', '--%s' % OPTIONS.HELP):
482            _usage_and_exit()
483        elif opt in ('-i', '--%s' % OPTIONS.ITERATIONS):
484            if arg.isdigit():
485                options[OPTIONS.ITERATIONS] = int(arg)
486            else:
487                _usage_and_exit()
488        elif opt in ('-m', '--%s' % OPTIONS.MODE):
489            arg = arg.lower()
490            if arg in MODE.GESTURE_PLAY_MODE:
491                options[OPTIONS.MODE] = arg
492            else:
493                print 'Warning: -m should be one of %s' % MODE.GESTURE_PLAY_MODE
494        elif opt in ('--%s' % OPTIONS.REPLAY, '--%s' % OPTIONS.RESUME):
495            log_dir = os.path.join(conf.log_root_dir, arg)
496            if os.path.isdir(log_dir):
497                # opt could be either '--replay' or '--resume'.
498                # We would like to strip off the '-' on the left hand side.
499                options[opt.lstrip('-')] = log_dir
500            else:
501                print 'Error: the log directory "%s" does not exist.' % log_dir
502                _usage_and_exit()
503        elif opt in ('-s', '--%s' % OPTIONS.SIMPLIFIED):
504            options[OPTIONS.SIMPLIFIED] = True
505        elif opt in ('--%s' % OPTIONS.SKIP_HTML,):
506            options[OPTIONS.SKIP_HTML] = True
507        elif opt in ('-t', '--%s' % OPTIONS.TOUCHSCREEN):
508            options[OPTIONS.TOUCHSCREEN] = True
509        elif opt in ('-u', '--%s' % OPTIONS.UPLOAD):
510            upload_to_gs(arg)
511            sys.exit()
512        else:
513            msg = 'This option "%s" is not supported.' % opt
514            _parsing_error(opt)
515
516    print 'Note: the %s mode is used.' % options[OPTIONS.MODE]
517    return options
518
519
520if __name__ == '__main__':
521    options = _parse_options()
522    fw = firmware_TouchMTB(options)
523    fw.main()
524