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