test_webrtc_peer_connection.py revision 464d00794fe192245c87877515d642244a182111
1import logging
2import os
3import time
4
5from autotest_lib.client.bin import utils
6from autotest_lib.client.common_lib import error
7from autotest_lib.client.common_lib.cros import chrome
8from autotest_lib.client.common_lib.cros import system_metrics_collector
9from autotest_lib.client.common_lib.cros import webrtc_utils
10from autotest_lib.client.cros.graphics import graphics_utils
11from autotest_lib.client.cros.video import helper_logger
12from telemetry.core import exceptions
13from telemetry.util import image_util
14
15
16EXTRA_BROWSER_ARGS = ['--use-fake-ui-for-media-stream',
17                      '--use-fake-device-for-media-stream']
18
19
20class WebRtcPeerConnectionTest(object):
21    """
22    Runs a WebRTC peer connection test.
23
24    This class runs a test that uses WebRTC peer connections to stress Chrome
25    and WebRTC. It interacts with HTML and JS files that contain the actual test
26    logic. It makes many assumptions about how these files behave. See one of
27    the existing tests and the documentation for run_test() for reference.
28    """
29    def __init__(
30            self,
31            title,
32            own_script,
33            common_script,
34            bindir,
35            tmpdir,
36            resultsdir,
37            timeout = 70,
38            test_runtime_seconds = 60,
39            num_peer_connections = 5,
40            iteration_delay_millis = 500,
41            before_start_hook = None):
42        """
43        Sets up a peer connection test.
44
45        @param title: Title of the test, shown on the test HTML page.
46        @param own_script: Name of the test's own JS file in bindir.
47        @param tmpdir: Directory to store tmp files, should be in the autotest
48                tree.
49        @param bindir: The directory that contains the test files and
50                own_script.
51        @param resultsdir: The directory to which results, e.g. screenshots,
52                should be written.
53        @param timeout: Timeout in seconds for the test.
54        @param test_runtime_seconds: How long to run the test. If errors occur
55                the test can exit earlier.
56        @param num_peer_connections: Number of peer connections to use.
57        @param iteration_delay_millis: delay in millis between each test
58                iteration.
59        @param before_start_hook: function accepting a Chrome browser tab as
60                argument. Is executed before the startTest() JS method call is
61                made.
62        """
63        self.title = title
64        self.own_script = own_script
65        self.common_script = common_script
66        self.bindir = bindir
67        self.tmpdir = tmpdir
68        self.resultsdir = resultsdir
69        self.timeout = timeout
70        self.test_runtime_seconds = test_runtime_seconds
71        self.num_peer_connections = num_peer_connections
72        self.iteration_delay_millis = iteration_delay_millis
73        self.before_start_hook = before_start_hook
74        self.tab = None
75
76    def start_test(self, cr, html_file):
77        """
78        Opens the test page.
79
80        @param cr: Autotest Chrome instance.
81        @param html_file: File object containing the HTML code to use in the
82                test. The html file needs to have the following JS methods:
83                startTest(runtimeSeconds, numPeerConnections, iterationDelay)
84                        Starts the test. Arguments are all numbers.
85                testRunner.getStatus()
86                        Gets the status of the test. Returns a string with the
87                        failure message. If the string starts with 'failure', it
88                        is interpreted as failure. The string 'ok-done' denotes
89                        that the test is complete.
90        """
91        self.tab = cr.browser.tabs[0]
92        self.tab.Navigate(cr.browser.platform.http_server.UrlOf(
93                os.path.join(self.bindir, html_file.name)))
94        self.tab.WaitForDocumentReadyStateToBeComplete()
95        if self.before_start_hook is not None:
96            self.before_start_hook(self.tab)
97        self.tab.EvaluateJavaScript(
98                "startTest(%d, %d, %d)" % (
99                        self.test_runtime_seconds,
100                        self.num_peer_connections,
101                        self.iteration_delay_millis))
102
103    def _test_done(self):
104        """
105        Determines if the test is done or not.
106
107        Does so by querying status of the JavaScript test runner.
108        @return True if the test is done, false if it is still in progress.
109        @raise TestFail if the status check returns a failure status.
110        """
111        status = self.tab.EvaluateJavaScript('testRunner.getStatus()')
112        if status.startswith('failure'):
113            raise error.TestFail(
114                    'Test status starts with failure, status is: ' + status)
115        logging.debug(status)
116        return status == 'ok-done'
117
118    def wait_test_completed(self, timeout_secs):
119        """
120        Waits until the test is done.
121
122        @param timeout_secs Max time to wait in seconds.
123
124        @raises TestError on timeout, or javascript eval fails, or
125                error status from the testRunner.getStatus() JS method.
126        """
127        start_secs = time.time()
128        while not self._test_done():
129            spent_time = time.time() - start_secs
130            if spent_time > timeout_secs:
131                raise utils.TimeoutError(
132                        'Test timed out after {} seconds'.format(spent_time))
133            self.do_in_wait_loop()
134
135    def do_in_wait_loop(self):
136        """
137        Called repeatedly in a loop while the test waits for completion.
138
139        Subclasses can override and provide specific behavior.
140        """
141        time.sleep(1)
142
143    @helper_logger.video_log_wrapper
144    def run_test(self):
145        """
146        Starts the test and waits until it is completed.
147        """
148        with chrome.Chrome(extra_browser_args = EXTRA_BROWSER_ARGS + \
149                           [helper_logger.chrome_vmodule_flag()],
150                           init_network_controller = True) as cr:
151            own_script_path = os.path.join(
152                    self.bindir, self.own_script)
153            common_script_path = webrtc_utils.get_common_script_path(
154                    self.common_script)
155
156            # Create the URLs to the JS scripts to include in the html file.
157            # Normally we would use the http_server.UrlOf method. However,
158            # that requires starting the server first. The server reads
159            # all file contents on startup, meaning we must completely
160            # create the html file first. Hence we create the url
161            # paths relative to the common prefix, which will be used as the
162            # base of the server.
163            base_dir = os.path.commonprefix(
164                    [own_script_path, common_script_path])
165            base_dir = base_dir.rstrip('/')
166            own_script_url = own_script_path[len(base_dir):]
167            common_script_url = common_script_path[len(base_dir):]
168
169            html_file = webrtc_utils.create_temp_html_file(
170                    self.title,
171                    self.tmpdir,
172                    own_script_url,
173                    common_script_url)
174            # Don't bother deleting the html file, the autotest tmp dir will be
175            # cleaned up by the autotest framework.
176            try:
177                cr.browser.platform.SetHTTPServerDirectories(
178                    [own_script_path, html_file.name, common_script_path])
179                self.start_test(cr, html_file)
180                self.wait_test_completed(self.timeout)
181                self.verify_status_ok()
182            finally:
183                # Ensure we always have a screenshot, both when succesful and
184                # when failed - useful for debugging.
185                self.take_screenshots()
186
187    def verify_status_ok(self):
188        """
189        Verifies that the status of the test is 'ok-done'.
190
191        @raises TestError the status is different from 'ok-done'.
192        """
193        status = self.tab.EvaluateJavaScript('testRunner.getStatus()')
194        if status != 'ok-done':
195            raise error.TestFail('Failed: %s' % status)
196
197    def take_screenshots(self):
198        """
199        Takes screenshots using two different mechanisms.
200
201        Takes one screenshot using graphics_utils which is a really low level
202        api that works between the kernel and userspace. The advantage is that
203        this captures the entire screen regardless of Chrome state. Disadvantage
204        is that it does not always work.
205
206        Takes one screenshot of the current tab using Telemetry.
207
208        Saves the screenshot in the results directory.
209        """
210        # Replace spaces with _ and lowercase the screenshot name for easier
211        # tab completion in terminals.
212        screenshot_name = self.title.replace(' ', '-').lower() + '-screenshot'
213        self.take_graphics_utils_screenshot(screenshot_name)
214        self.take_browser_tab_screenshot(screenshot_name)
215
216    def take_graphics_utils_screenshot(self, screenshot_name):
217        """
218        Takes a screenshot of what is currently displayed.
219
220        Uses the low level graphics_utils API.
221
222        @param screenshot_name: Name of the screenshot.
223        """
224        try:
225            full_filename = screenshot_name + '_graphics_utils'
226            graphics_utils.take_screenshot(self.resultsdir, full_filename)
227        except RuntimeError as e:
228            logging.warn('Screenshot using graphics_utils failed', exc_info = e)
229
230    def take_browser_tab_screenshot(self, screenshot_name):
231        """
232        Takes a screenshot of the current browser tab.
233
234        @param screenshot_name: Name of the screenshot.
235        """
236        if self.tab is not None and self.tab.screenshot_supported:
237            try:
238                screenshot = self.tab.Screenshot(timeout = 10)
239                full_filename = os.path.join(
240                        self.resultsdir, screenshot_name + '_browser_tab.png')
241                image_util.WritePngFile(screenshot, full_filename)
242            except exceptions.Error as e:
243                # This can for example occur if Chrome crashes. It will
244                # cause the Screenshot call to timeout.
245                logging.warn(
246                        'Screenshot using telemetry tab.Screenshot failed',
247                        exc_info = e)
248        else:
249            logging.warn(
250                    'Screenshot using telemetry tab.Screenshot() not supported')
251
252
253
254class WebRtcPeerConnectionPerformanceTest(WebRtcPeerConnectionTest):
255    """
256    Runs a WebRTC performance test.
257    """
258    def __init__(
259            self,
260            title,
261            own_script,
262            common_script,
263            bindir,
264            tmpdir,
265            resultsdir,
266            timeout = 70,
267            test_runtime_seconds = 60,
268            num_peer_connections = 5,
269            iteration_delay_millis = 500,
270            before_start_hook = None):
271          super(WebRtcPeerConnectionPerformanceTest, self).__init__(
272                  title,
273                  own_script,
274                  common_script,
275                  bindir,
276                  tmpdir,
277                  resultsdir,
278                  timeout,
279                  test_runtime_seconds,
280                  num_peer_connections,
281                  iteration_delay_millis,
282                  before_start_hook)
283          self.collector = system_metrics_collector.SystemMetricsCollector()
284
285    def do_in_wait_loop(self):
286        self.collector.collect_snapshot()
287        time.sleep(1)
288
289