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