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            if self.options[OPTIONS.MODE] == MODE.CALIBRATION:
318                print ('Please upload the raw data to the spreadsheet after '
319                       'the calibration tests have been finished successfully:')
320                print '$ python spreadsheet.py -v'
321
322
323def upload_to_gs(log_dir):
324    """Upload the gesture event files specified in log_dir to Google cloud
325    storage server.
326
327    @param log_dir: the log directory of which the gesture event files are
328            to be uploaded to Google cloud storage server
329    """
330    # Set up gsutil package.
331    # The board argument is used to locate the proper bucket directory
332    gs = cros_gs.CrosGs(firmware_utils.get_board())
333
334    log_path = os.path.join(conf.log_root_dir, log_dir)
335    if not os.path.isdir(log_path):
336        print_and_exit('Error: the log path "%s" does not exist.' % log_path)
337
338    print 'Uploading "%s" to %s ...\n' % (log_path, gs.bucket)
339    try:
340        gs.upload(log_path)
341    except Exception, e:
342        msg = 'Error in uploading event files in %s: %s.'
343        print_and_exit(msg % (log_path, e))
344
345
346def _usage_and_exit():
347    """Print the usage of this program."""
348    print 'Usage: $ DISPLAY=:0 [OPTIONS="options"] python %s\n' % sys.argv[0]
349    print 'options:'
350    print '  -d, --%s' % OPTIONS.DEVICE
351    print '        use the system device for replay'
352    print '  -h, --%s' % OPTIONS.HELP
353    print '        show this help'
354    print '  -i, --%s iterations' % OPTIONS.ITERATIONS
355    print '        specify the number of iterations'
356    print '  -f, --%s' % OPTIONS.FNGENERATOR
357    print '        Indicate that (despite not having a touchbot) there is a'
358    print '        function generator attached for the noise tests'
359    print '  -m, --%s mode' % OPTIONS.MODE
360    print '        specify the gesture playing mode'
361    print '        mode could be one of the following options'
362    print '            calibration: conducting pressure calibration'
363    print '            complete: all gestures including those in ' \
364                                'both manual mode and robot mode'
365    print '            manual: all gestures minus gestures in robot mode'
366    print '            noise: an extensive, 4 hour noise test'
367    print '            robot: using robot to perform gestures automatically'
368    print '            robot_sim: robot simulation, for developer only'
369    print '  --%s log_dir' % OPTIONS.REPLAY
370    print '        Replay the gesture files and get the test results.'
371    print '        log_dir is a log sub-directory in %s' % conf.log_root_dir
372    print '  --%s log_dir' % OPTIONS.RESUME
373    print '        Resume recording the gestures files in the log_dir.'
374    print '        log_dir is a log sub-directory in %s' % conf.log_root_dir
375    print '  -s, --%s' % OPTIONS.SIMPLIFIED
376    print '        Use one variation per gesture'
377    print '  --%s' % OPTIONS.SKIP_HTML
378    print '        Do not show the html test result.'
379    print '  -t, --%s' % OPTIONS.TOUCHSCREEN
380    print '        Use the touchscreen instead of a touchpad'
381    print '  -u, --%s log_dir' % OPTIONS.UPLOAD
382    print '        Upload the gesture event files in the specified log_dir '
383    print '        to Google cloud storage server.'
384    print '        It uploads results that you already have from a previous run'
385    print '        without re-running the test.'
386    print '        log_dir could be either '
387    print '        (1) a directory in %s' % conf.log_root_dir
388    print '        (2) a full path, or'
389    print '        (3) the default "latest" directory in %s if omitted' % \
390                   conf.log_root_dir
391    print
392    print 'Example:'
393    print '  # Use the robot to perform 3 iterations of the robot gestures.'
394    print '  $ DISPLAY=:0 OPTIONS="-m robot_sim -i 3" python main.py\n'
395    print '  # Perform 1 iteration of the manual gestures.'
396    print '  $ DISPLAY=:0 OPTIONS="-m manual" python main.py\n'
397    print '  # Perform 1 iteration of all manual and robot gestures.'
398    print '  $ DISPLAY=:0 OPTIONS="-m complete" python main.py\n'
399    print '  # Perform pressure calibration.'
400    print '  $ DISPLAY=:0 OPTIONS="-m calibration" python main.py\n'
401    print '  # Use the robot to perform a latency test with Quickstep'
402    print '  $ DISPLAY=:0 OPTIONS="-m quickstep" python main.py\n'
403    print '  # Use the robot to perform an extensive, 4 hour noise test'
404    print '  $ DISPLAY=:0 OPTIONS="-m noise" python main.py\n'
405    print '  # Replay the gesture files in the latest log directory.'
406    print '  $ DISPLAY=:0 OPTIONS="--replay latest" python main.py\n'
407    example_log_dir = '20130226_040802-fw_1.2-manual'
408    print ('  # Replay the gesture files in %s/%s with a mocked device.' %
409            (conf.log_root_dir, example_log_dir))
410    print '  $ DISPLAY=:0 OPTIONS="--replay %s" python main.py\n' % \
411            example_log_dir
412    print ('  # Replay the gesture files in %s/%s with the system device.' %
413            (conf.log_root_dir, example_log_dir))
414    print ('  $ DISPLAY=:0 OPTIONS="--replay %s -d" python main.py\n' %
415            example_log_dir)
416    print '  # Resume recording the gestures in the latest log directory.'
417    print '  $ DISPLAY=:0 OPTIONS="--resume latest" python main.py\n'
418    print '  # Resume recording the gestures in %s/%s.' % (conf.log_root_dir,
419                                                           example_log_dir)
420    print '  $ DISPLAY=:0 OPTIONS="--resume %s" python main.py\n' % \
421            example_log_dir
422    print ('  # Upload the gesture event files specified in the log_dir '
423             'to Google cloud storage server.')
424    print ('  $ DISPLAY=:0 OPTIONS="-u 20130701_020120-fw_11.23-complete" '
425           'python main.py\n')
426    print ('  # Upload the gesture event files in the "latest" directory '
427           'to Google cloud storage server.')
428    print '  $ DISPLAY=:0 OPTIONS="-u latest" python main.py\n'
429
430    sys.exit(1)
431
432
433def _parsing_error(msg):
434    """Print the usage and exit when encountering parsing error."""
435    print 'Error: %s' % msg
436    _usage_and_exit()
437
438
439def _parse_options():
440    """Parse the options.
441
442    Note that the options are specified with environment variable OPTIONS,
443    because pyauto seems not compatible with command line options.
444    """
445    # Set the default values of options.
446    options = {OPTIONS.DEVICE: False,
447               OPTIONS.FNGENERATOR: False,
448               OPTIONS.ITERATIONS: 1,
449               OPTIONS.MODE: MODE.MANUAL,
450               OPTIONS.REPLAY: None,
451               OPTIONS.RESUME: None,
452               OPTIONS.SIMPLIFIED: False,
453               OPTIONS.SKIP_HTML: False,
454               OPTIONS.TOUCHSCREEN: False,
455               OPTIONS.UPLOAD: None,
456    }
457
458    # Get the command line options or get the options from environment OPTIONS
459    options_list = sys.argv[1:] or os.environ.get('OPTIONS', '').split()
460    if not options_list:
461        return options
462
463    short_opt = 'dfhi:m:stu:'
464    long_opt = [OPTIONS.DEVICE,
465                OPTIONS.FNGENERATOR,
466                OPTIONS.HELP,
467                OPTIONS.ITERATIONS + '=',
468                OPTIONS.MODE + '=',
469                OPTIONS.REPLAY + '=',
470                OPTIONS.RESUME + '=',
471                OPTIONS.SIMPLIFIED,
472                OPTIONS.SKIP_HTML,
473                OPTIONS.TOUCHSCREEN,
474                OPTIONS.UPLOAD + '=',
475    ]
476    try:
477        opts, args = getopt.getopt(options_list, short_opt, long_opt)
478    except getopt.GetoptError, err:
479        _parsing_error(str(err))
480
481    for opt, arg in opts:
482        if opt in ('-d', '--%s' % OPTIONS.DEVICE):
483            options[OPTIONS.DEVICE] = True
484        if opt in ('-f', '--%s' % OPTIONS.FNGENERATOR):
485            options[OPTIONS.FNGENERATOR] = True
486        elif opt in ('-h', '--%s' % OPTIONS.HELP):
487            _usage_and_exit()
488        elif opt in ('-i', '--%s' % OPTIONS.ITERATIONS):
489            if arg.isdigit():
490                options[OPTIONS.ITERATIONS] = int(arg)
491            else:
492                _usage_and_exit()
493        elif opt in ('-m', '--%s' % OPTIONS.MODE):
494            arg = arg.lower()
495            if arg in MODE.GESTURE_PLAY_MODE:
496                options[OPTIONS.MODE] = arg
497            else:
498                print 'Warning: -m should be one of %s' % MODE.GESTURE_PLAY_MODE
499        elif opt in ('--%s' % OPTIONS.REPLAY, '--%s' % OPTIONS.RESUME):
500            log_dir = os.path.join(conf.log_root_dir, arg)
501            if os.path.isdir(log_dir):
502                # opt could be either '--replay' or '--resume'.
503                # We would like to strip off the '-' on the left hand side.
504                options[opt.lstrip('-')] = log_dir
505            else:
506                print 'Error: the log directory "%s" does not exist.' % log_dir
507                _usage_and_exit()
508        elif opt in ('-s', '--%s' % OPTIONS.SIMPLIFIED):
509            options[OPTIONS.SIMPLIFIED] = True
510        elif opt in ('--%s' % OPTIONS.SKIP_HTML,):
511            options[OPTIONS.SKIP_HTML] = True
512        elif opt in ('-t', '--%s' % OPTIONS.TOUCHSCREEN):
513            options[OPTIONS.TOUCHSCREEN] = True
514        elif opt in ('-u', '--%s' % OPTIONS.UPLOAD):
515            upload_to_gs(arg)
516            sys.exit()
517        else:
518            msg = 'This option "%s" is not supported.' % opt
519            _parsing_error(opt)
520
521    print 'Note: the %s mode is used.' % options[OPTIONS.MODE]
522    return options
523
524
525if __name__ == '__main__':
526    options = _parse_options()
527    fw = firmware_TouchMTB(options)
528    fw.main()
529