1# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4import glob, logging, os
5
6from autotest_lib.client.bin import test, utils
7from autotest_lib.client.common_lib import base_utils, error
8from autotest_lib.client.cros.graphics import graphics_utils
9
10def get_percent_difference(file1, file2):
11    """
12    Performs byte-by-byte comparison of two files, given by their paths |file1|
13    and |file2|.  Returns difference as a percentage of the total file size.  If
14    one file is larger than the other, the difference is a percentage of
15    |file1|.
16    """
17    files = (file1, file2)
18    sizes = {}
19    for filename in files:
20        if not os.path.exists(filename):
21            raise error.TestFail('Could not find file \'%s\'.' % filename)
22        sizes[filename] = os.path.getsize(filename)
23        if sizes[filename] == 0:
24            raise error.TestFail('File \'%s\' has zero size.' % filename)
25
26    diff_bytes = int(utils.system_output('cmp -l %s %s | wc -l' % files))
27
28    return round(100. * diff_bytes / sizes[file1])
29
30
31class graphics_VTSwitch(test.test):
32    """
33    Verify that VT switching works.
34    """
35    version = 1
36    GSC = None
37    # TODO(crosbug.com/36417): Need to handle more than one display screen.
38
39    def setup(self):
40        self.job.setup_dep(['gfxtest'])
41
42    def initialize(self):
43        self.GSC = graphics_utils.GraphicsStateChecker()
44
45    def cleanup(self):
46        # Return to VT1 when done.  Ideally, the screen should already be in VT1
47        # but the test might fail and terminate while in VT2.
48        self._switch_to_vt(1)
49        if self.GSC:
50            self.GSC.finalize()
51
52    def run_once(self,
53                 num_iterations=2,
54                 similarity_percent_threshold=95,
55                 difference_percent_threshold=5):
56        # TODO(ihf): Remove this once VTSwitch works on freon.
57        if utils.is_freon():
58            raise error.TestNAError(
59                    'Test needs work on Freon. See crbug.com/413088.')
60
61        self._num_errors = 0
62        keyvals = {}
63
64        # Make sure we start in VT1
65        if not self._switch_to_vt(1):
66            raise error.TestFail('Could not switch to VT1')
67
68        # Take screenshot of sign-in screen.
69        logged_out_screenshot = self._take_current_vt_screenshot()
70
71        keyvals['num_iterations'] = num_iterations
72
73        # Go to VT2 and take a screenshot.
74        if not self._switch_to_vt(2):
75            raise error.TestFail('Could not switch to VT2')
76        vt2_screenshot = self._take_current_vt_screenshot()
77
78        # Make sure VT1 and VT2 are sufficiently different.
79        diff = get_percent_difference(logged_out_screenshot, vt2_screenshot)
80        keyvals['percent_initial_VT1_VT2_difference'] = diff
81        if not diff >= difference_percent_threshold:
82            self._num_errors += 1
83            logging.error('VT1 and VT2 screenshots only differ by ' + \
84                          '%d %%: %s vs %s' %
85                          (diff, logged_out_screenshot, vt2_screenshot))
86
87        num_identical_vt1_screenshots = 0
88        num_identical_vt2_screenshots = 0
89        max_vt1_difference_percent = 0
90        max_vt2_difference_percent = 0
91
92        # Repeatedly switch between VT1 and VT2.
93        for iteration in xrange(num_iterations):
94            logging.info('Iteration #%d', iteration)
95
96            # Go to VT1 and take a screenshot.
97            self._switch_to_vt(1)
98            current_vt1_screenshot = self._take_current_vt_screenshot()
99
100            # Make sure the current VT1 screenshot is the same as (or similar
101            # to) the original login screen screenshot.
102            diff = get_percent_difference(logged_out_screenshot,
103                                          current_vt1_screenshot)
104            if not diff < similarity_percent_threshold:
105                max_vt1_difference_percent = \
106                    max(diff, max_vt1_difference_percent)
107                self._num_errors += 1
108                logging.error('VT1 screenshots differ by %d %%: %s vs %s',
109                              diff, logged_out_screenshot,
110                              current_vt1_screenshot)
111            else:
112                num_identical_vt1_screenshots += 1
113
114            # Go to VT2 and take a screenshot.
115            self._switch_to_vt(2)
116            current_vt2_screenshot = self._take_current_vt_screenshot()
117
118            # Make sure the current VT2 screenshot is the same as (or similar
119            # to) the first VT2 screenshot.
120            diff = get_percent_difference(vt2_screenshot,
121                                          current_vt2_screenshot)
122            if not diff <= similarity_percent_threshold:
123                max_vt2_difference_percent = \
124                    max(diff, max_vt2_difference_percent)
125                self._num_errors += 1
126                logging.error(
127                    'VT2 screenshots differ by %d %%: %s vs %s',
128                    diff, vt2_screenshot, current_vt2_screenshot)
129            else:
130                num_identical_vt2_screenshots += 1
131
132        self._switch_to_vt(1)
133
134        keyvals['percent_VT1_screenshot_max_difference'] = \
135            max_vt1_difference_percent
136        keyvals['percent_VT2_screenshot_max_difference'] = \
137            max_vt2_difference_percent
138        keyvals['num_identical_vt1_screenshots'] = num_identical_vt1_screenshots
139        keyvals['num_identical_vt2_screenshots'] = num_identical_vt2_screenshots
140
141        self.write_perf_keyval(keyvals)
142
143        if self._num_errors > 0:
144            raise error.TestError('Test failed with %d errors' %
145                                  self._num_errors)
146
147
148    def _take_current_vt_screenshot(self):
149        """
150        Captures a screenshot of the current VT screen in BMP format.
151        Returns the path of the screenshot file.
152        """
153        current_vt = int(utils.system_output('fgconsole'))
154        extension = 'bmp'
155
156        # In VT1, X is running so use that screenshot function.
157        if current_vt == 1:
158            return graphics_utils.take_screenshot(self.resultsdir,
159                                                  'graphics_VTSwitch_VT1',
160                                                  extension)
161
162        # Otherwise, grab the framebuffer using DRM.
163        prefix = 'graphics_VTSwitch_VT2'
164        next_index = len(glob.glob(
165            os.path.join(self.resultsdir, '%s-*.%s' % (prefix, extension))))
166        filename = '%s-%d.%s' % (prefix, next_index, extension)
167        output_path = os.path.join(self.resultsdir, filename)
168        return self._take_drm_screenshot(output_path)
169
170
171    def _take_drm_screenshot(self, output_path):
172        """
173        Takes drm screenshot.
174        """
175        autotest_deps_path = os.path.join(self.autodir, 'deps')
176        getfb_path = os.path.join(autotest_deps_path, 'gfxtest', 'getfb')
177        output = utils.system_output('%s %s.rgba' % (getfb_path, output_path))
178        for line in output.split('\n'):
179            # Parse the getfb output for info about framebuffer size.  The line
180            # should looks omething like:
181            #   Framebuffer info: 1024x768, 32bpp
182            if line.startswith('Framebuffer info:'):
183                size = line.split(':')[1].split(',')[0].strip()
184                break
185        utils.system('convert -depth 8 -size %s %s.rgba %s' %
186                     (size, output_path, output_path))
187
188        logging.info('Saving screenshot to %s', output_path)
189        return output_path
190
191
192    def _switch_to_vt(self, vt):
193        """
194        Switches to virtual terminal given by |vt| (1, 2, etc) and checks that
195        the switch was successful by calling fgconsole.
196
197        Returns True if fgconsole returned the new vt number, False otherwise.
198        """
199        utils.system_output('chvt %d' % vt)
200
201        # Verify that the VT switch was successful.
202        current_vt = base_utils.wait_for_value(
203            lambda: int(utils.system_output('fgconsole')),
204            expected_value=vt)
205        if vt != current_vt:
206            self._num_errors += 1
207            logging.error('Current VT %d does not match expected VT %d',
208                          current_vt, vt)
209            return False
210        logging.info('Switched to VT%d', vt)
211        return True
212