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.multimedia import system_facade_native
12from autotest_lib.client.cros.video import helper_logger
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            debugdir,
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 debugdir: The directory to which debug data, 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.debugdir = debugdir
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                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. This method should not throw
90                        an exception.
91        """
92        self.tab = cr.browser.tabs[0]
93        self.tab.Navigate(cr.browser.platform.http_server.UrlOf(
94                os.path.join(self.bindir, html_file.name)))
95        self.tab.WaitForDocumentReadyStateToBeComplete()
96        if self.before_start_hook is not None:
97            self.before_start_hook(self.tab)
98        self.tab.EvaluateJavaScript(
99                "startTest(%d, %d, %d)" % (
100                        self.test_runtime_seconds,
101                        self.num_peer_connections,
102                        self.iteration_delay_millis))
103
104    def stop_test(self):
105        """
106        Hook that always get called after the test has run.
107        """
108        pass
109
110    def _test_done(self):
111        """
112        Determines if the test is done or not.
113
114        Does so by querying status of the JavaScript test runner.
115        @return True if the test is done, false if it is still in progress.
116        @raise TestFail if the status check returns a failure status.
117        """
118        status = self.tab.EvaluateJavaScript('getStatus()')
119        if status.startswith('failure'):
120            raise error.TestFail(
121                    'Test status starts with failure, status is: ' + status)
122        logging.debug(status)
123        return status == 'ok-done'
124
125    def wait_test_completed(self, timeout_secs):
126        """
127        Waits until the test is done.
128
129        @param timeout_secs Max time to wait in seconds.
130
131        @raises TestError on timeout, or javascript eval fails, or
132                error status from the getStatus() JS method.
133        """
134        start_secs = time.time()
135        while not self._test_done():
136            spent_time = time.time() - start_secs
137            if spent_time > timeout_secs:
138                raise utils.TimeoutError(
139                        'Test timed out after {} seconds'.format(spent_time))
140            self.do_in_wait_loop()
141
142    def do_in_wait_loop(self):
143        """
144        Called repeatedly in a loop while the test waits for completion.
145
146        Subclasses can override and provide specific behavior.
147        """
148        time.sleep(1)
149
150    @helper_logger.video_log_wrapper
151    def run_test(self):
152        """
153        Starts the test and waits until it is completed.
154        """
155        with chrome.Chrome(extra_browser_args = EXTRA_BROWSER_ARGS + \
156                           [helper_logger.chrome_vmodule_flag()],
157                           init_network_controller = True) as cr:
158            own_script_path = os.path.join(
159                    self.bindir, self.own_script)
160            common_script_path = webrtc_utils.get_common_script_path(
161                    self.common_script)
162
163            # Create the URLs to the JS scripts to include in the html file.
164            # Normally we would use the http_server.UrlOf method. However,
165            # that requires starting the server first. The server reads
166            # all file contents on startup, meaning we must completely
167            # create the html file first. Hence we create the url
168            # paths relative to the common prefix, which will be used as the
169            # base of the server.
170            base_dir = os.path.commonprefix(
171                    [own_script_path, common_script_path])
172            base_dir = base_dir.rstrip('/')
173            own_script_url = own_script_path[len(base_dir):]
174            common_script_url = common_script_path[len(base_dir):]
175
176            html_file = webrtc_utils.create_temp_html_file(
177                    self.title,
178                    self.tmpdir,
179                    own_script_url,
180                    common_script_url)
181            # Don't bother deleting the html file, the autotest tmp dir will be
182            # cleaned up by the autotest framework.
183            try:
184                cr.browser.platform.SetHTTPServerDirectories(
185                    [own_script_path, html_file.name, common_script_path])
186                self.start_test(cr, html_file)
187                self.wait_test_completed(self.timeout)
188                self.verify_status_ok()
189            finally:
190                # Ensure we always have a screenshot, both when succesful and
191                # when failed - useful for debugging.
192                self.take_screenshots()
193                self.stop_test()
194
195    def verify_status_ok(self):
196        """
197        Verifies that the status of the test is 'ok-done'.
198
199        @raises TestError the status is different from 'ok-done'.
200        """
201        status = self.tab.EvaluateJavaScript('getStatus()')
202        if status != 'ok-done':
203            raise error.TestFail('Failed: %s' % status)
204
205    def take_screenshots(self):
206        """
207        Takes screenshots using two different mechanisms.
208
209        Takes one screenshot using graphics_utils which is a really low level
210        api that works between the kernel and userspace. The advantage is that
211        this captures the entire screen regardless of Chrome state. Disadvantage
212        is that it does not always work.
213
214        Takes one screenshot of the current tab using Telemetry.
215
216        Saves the screenshot in the results directory.
217        """
218        # Replace spaces with _ and lowercase the screenshot name for easier
219        # tab completion in terminals.
220        screenshot_name = self.title.replace(' ', '-').lower() + '-screenshot'
221        self.take_graphics_utils_screenshot(screenshot_name)
222        self.take_browser_tab_screenshot(screenshot_name)
223
224    def take_graphics_utils_screenshot(self, screenshot_name):
225        """
226        Takes a screenshot of what is currently displayed.
227
228        Uses the low level graphics_utils API.
229
230        @param screenshot_name: Name of the screenshot.
231        """
232        try:
233            full_filename = screenshot_name + '_graphics_utils'
234            graphics_utils.take_screenshot(self.debugdir, full_filename)
235        except StandardError as e:
236            logging.warn('Screenshot using graphics_utils failed', exc_info = e)
237
238    def take_browser_tab_screenshot(self, screenshot_name):
239        """
240        Takes a screenshot of the current browser tab.
241
242        @param screenshot_name: Name of the screenshot.
243        """
244        if self.tab is not None and self.tab.screenshot_supported:
245            try:
246                screenshot = self.tab.Screenshot(timeout = 10)
247                full_filename = os.path.join(
248                        self.debugdir, screenshot_name + '_browser_tab.png')
249                image_util.WritePngFile(screenshot, full_filename)
250            except Exception:
251                # This can for example occur if Chrome crashes. It will
252                # cause the Screenshot call to timeout.
253                logging.warn(
254                        'Screenshot using telemetry tab.Screenshot failed',
255                        exc_info=True)
256        else:
257            logging.warn(
258                    'Screenshot using telemetry tab.Screenshot() not supported')
259
260
261
262class WebRtcPeerConnectionPerformanceTest(WebRtcPeerConnectionTest):
263    """
264    Runs a WebRTC performance test.
265    """
266    def __init__(
267            self,
268            title,
269            own_script,
270            common_script,
271            bindir,
272            tmpdir,
273            debugdir,
274            timeout = 70,
275            test_runtime_seconds = 60,
276            num_peer_connections = 5,
277            iteration_delay_millis = 500,
278            before_start_hook = None):
279
280          def perf_before_start_hook(tab):
281              """
282              Before start hook to disable cpu overuse detection.
283              """
284              if before_start_hook:
285                  before_start_hook(tab)
286              tab.EvaluateJavaScript('cpuOveruseDetection = false')
287
288          super(WebRtcPeerConnectionPerformanceTest, self).__init__(
289                  title,
290                  own_script,
291                  common_script,
292                  bindir,
293                  tmpdir,
294                  debugdir,
295                  timeout,
296                  test_runtime_seconds,
297                  num_peer_connections,
298                  iteration_delay_millis,
299                  perf_before_start_hook)
300          self.collector = system_metrics_collector.SystemMetricsCollector(
301                system_facade_native.SystemFacadeNative())
302          # TODO(crbug/784365): If this proves to work fine, move to a separate
303          # module and make more generic.
304          delay = 5
305          iterations = self.test_runtime_seconds / delay + 1
306          utils.BgJob('top -b -d %d -n %d -w 512 -c > %s/top_output.txt'
307                      % (delay, iterations, self.debugdir))
308          utils.BgJob('iostat -x %d %d > %s/iostat_output.txt'
309                      % (delay, iterations, self.debugdir))
310          utils.BgJob('for i in $(seq %d);'
311                      'do netstat -s >> %s/netstat_output.txt'
312                      ';sleep %d;done'
313                      % (delay, self.debugdir, iterations))
314
315    def start_test(self, cr, html_file):
316        super(WebRtcPeerConnectionPerformanceTest, self).start_test(
317                cr, html_file)
318        self.collector.pre_collect()
319
320    def stop_test(self):
321        self.collector.post_collect()
322        super(WebRtcPeerConnectionPerformanceTest, self).stop_test()
323
324    def do_in_wait_loop(self):
325        self.collector.collect_snapshot()
326        time.sleep(1)
327