1# Copyright 2012 The Chromium 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. 4 5from telemetry.core import command_line 6from telemetry.page import test_expectations 7from telemetry.page.actions import action_runner as action_runner_module 8 9 10class Failure(Exception): 11 """Exception that can be thrown from PageTest to indicate an 12 undesired but designed-for problem.""" 13 14 15class TestNotSupportedOnPlatformFailure(Failure): 16 """Exception that can be thrown to indicate that a certain feature required 17 to run the test is not available on the platform, hardware configuration, or 18 browser version.""" 19 20 21class MeasurementFailure(Failure): 22 """Exception that can be thrown from MeasurePage to indicate an undesired but 23 designed-for problem.""" 24 25 26class PageTest(command_line.Command): 27 """A class styled on unittest.TestCase for creating page-specific tests. 28 29 Test should override ValidateAndMeasurePage to perform test 30 validation and page measurement as necessary. 31 32 class BodyChildElementMeasurement(PageTest): 33 def ValidateAndMeasurePage(self, page, tab, results): 34 body_child_count = tab.EvaluateJavaScript( 35 'document.body.children.length') 36 results.AddValue(scalar.ScalarValue( 37 page, 'body_children', 'count', body_child_count)) 38 39 The class also provide hooks to add test-specific options. Here is 40 an example: 41 42 class BodyChildElementMeasurement(PageTest): 43 def AddCommandLineArgs(parser): 44 parser.add_option('--element', action='store', default='body') 45 46 def ValidateAndMeasurePage(self, page, tab, results): 47 body_child_count = tab.EvaluateJavaScript( 48 'document.querySelector('%s').children.length') 49 results.AddValue(scalar.ScalarValue( 50 page, 'children', 'count', child_count)) 51 52 Args: 53 action_name_to_run: This is the method name in telemetry.page.Page 54 subclasses to run. 55 discard_first_run: Discard the first run of this page. This is 56 usually used with page_repeat and pageset_repeat options. 57 attempts: The number of attempts to run if we encountered 58 infrastructure problems (as opposed to test issues), such as 59 losing a browser. 60 max_failures: The number of page failures allowed before we stop 61 running other pages. 62 is_action_name_to_run_optional: Determines what to do if 63 action_name_to_run is not empty but the page doesn't have that 64 action. The page will run (without any action) if 65 is_action_name_to_run_optional is True, otherwise the page 66 will fail. 67 """ 68 69 options = {} 70 71 def __init__(self, 72 action_name_to_run='', 73 needs_browser_restart_after_each_page=False, 74 discard_first_result=False, 75 clear_cache_before_each_run=False, 76 attempts=3, 77 max_failures=None, 78 is_action_name_to_run_optional=False): 79 super(PageTest, self).__init__() 80 81 self.options = None 82 if action_name_to_run: 83 assert action_name_to_run.startswith('Run') \ 84 and '_' not in action_name_to_run, \ 85 ('Wrong way of naming action_name_to_run. By new convention,' 86 'action_name_to_run must start with Run- prefix and in CamelCase.') 87 self._action_name_to_run = action_name_to_run 88 self._needs_browser_restart_after_each_page = ( 89 needs_browser_restart_after_each_page) 90 self._discard_first_result = discard_first_result 91 self._clear_cache_before_each_run = clear_cache_before_each_run 92 self._close_tabs_before_run = True 93 self._attempts = attempts 94 self._max_failures = max_failures 95 self._is_action_name_to_run_optional = is_action_name_to_run_optional 96 assert self._attempts > 0, 'Test attempts must be greater than 0' 97 # If the test overrides the TabForPage method, it is considered a multi-tab 98 # test. The main difference between this and a single-tab test is that we 99 # do not attempt recovery for the former if a tab or the browser crashes, 100 # because we don't know the current state of tabs (how many are open, etc.) 101 self.is_multi_tab_test = (self.__class__ is not PageTest and 102 self.TabForPage.__func__ is not 103 self.__class__.__bases__[0].TabForPage.__func__) 104 # _exit_requested is set to true when the test requests an early exit. 105 self._exit_requested = False 106 107 @classmethod 108 def SetArgumentDefaults(cls, parser): 109 parser.set_defaults(**cls.options) 110 111 @property 112 def discard_first_result(self): 113 """When set to True, the first run of the test is discarded. This is 114 useful for cases where it's desirable to have some test resource cached so 115 the first run of the test can warm things up. """ 116 return self._discard_first_result 117 118 @discard_first_result.setter 119 def discard_first_result(self, discard): 120 self._discard_first_result = discard 121 122 @property 123 def clear_cache_before_each_run(self): 124 """When set to True, the browser's disk and memory cache will be cleared 125 before each run.""" 126 return self._clear_cache_before_each_run 127 128 @property 129 def close_tabs_before_run(self): 130 """When set to True, all tabs are closed before running the test for the 131 first time.""" 132 return self._close_tabs_before_run 133 134 @close_tabs_before_run.setter 135 def close_tabs_before_run(self, close_tabs): 136 self._close_tabs_before_run = close_tabs 137 138 @property 139 def attempts(self): 140 """Maximum number of times test will be attempted.""" 141 return self._attempts 142 143 @attempts.setter 144 def attempts(self, count): 145 assert self._attempts > 0, 'Test attempts must be greater than 0' 146 self._attempts = count 147 148 @property 149 def max_failures(self): 150 """Maximum number of failures allowed for the page set.""" 151 return self._max_failures 152 153 @max_failures.setter 154 def max_failures(self, count): 155 self._max_failures = count 156 157 def Run(self, args): 158 # Define this method to avoid pylint errors. 159 # TODO(dtu): Make this actually run the test with args.page_set. 160 pass 161 162 def RestartBrowserBeforeEachPage(self): 163 """ Should the browser be restarted for the page? 164 165 This returns true if the test needs to unconditionally restart the 166 browser for each page. It may be called before the browser is started. 167 """ 168 return self._needs_browser_restart_after_each_page 169 170 def StopBrowserAfterPage(self, browser, page): # pylint: disable=W0613 171 """Should the browser be stopped after the page is run? 172 173 This is called after a page is run to decide whether the browser needs to 174 be stopped to clean up its state. If it is stopped, then it will be 175 restarted to run the next page. 176 177 A test that overrides this can look at both the page and the browser to 178 decide whether it needs to stop the browser. 179 """ 180 return False 181 182 def CustomizeBrowserOptions(self, options): 183 """Override to add test-specific options to the BrowserOptions object""" 184 185 def CustomizeBrowserOptionsForSinglePage(self, page, options): 186 """Set options specific to the test and the given page. 187 188 This will be called with the current page when the browser is (re)started. 189 Changing options at this point only makes sense if the browser is being 190 restarted for each page. Note that if page has a startup_url, the browser 191 will always be restarted for each run. 192 """ 193 if page.startup_url: 194 options.browser_options.startup_url = page.startup_url 195 196 def WillStartBrowser(self, platform): 197 """Override to manipulate the browser environment before it launches.""" 198 199 def DidStartBrowser(self, browser): 200 """Override to customize the browser right after it has launched.""" 201 202 def CanRunForPage(self, page): # pylint: disable=W0613 203 """Override to customize if the test can be ran for the given page.""" 204 if self._action_name_to_run and not self._is_action_name_to_run_optional: 205 return hasattr(page, self._action_name_to_run) 206 return True 207 208 def WillRunTest(self, options): 209 """Override to do operations before the page set(s) are navigated.""" 210 self.options = options 211 212 def DidRunTest(self, browser, results): # pylint: disable=W0613 213 """Override to do operations after all page set(s) are completed. 214 215 This will occur before the browser is torn down. 216 """ 217 self.options = None 218 219 def WillNavigateToPage(self, page, tab): 220 """Override to do operations before the page is navigated, notably Telemetry 221 will already have performed the following operations on the browser before 222 calling this function: 223 * Ensure only one tab is open. 224 * Call WaitForDocumentReadyStateToComplete on the tab.""" 225 226 def DidNavigateToPage(self, page, tab): 227 """Override to do operations right after the page is navigated and after 228 all waiting for completion has occurred.""" 229 230 def WillRunActions(self, page, tab): 231 """Override to do operations before running the actions on the page.""" 232 233 def DidRunActions(self, page, tab): 234 """Override to do operations after running the actions on the page.""" 235 236 def CleanUpAfterPage(self, page, tab): 237 """Called after the test run method was run, even if it failed.""" 238 239 def CreateExpectations(self, page_set): # pylint: disable=W0613 240 """Override to make this test generate its own expectations instead of 241 any that may have been defined in the page set.""" 242 return test_expectations.TestExpectations() 243 244 def TabForPage(self, page, browser): # pylint: disable=W0613 245 """Override to select a different tab for the page. For instance, to 246 create a new tab for every page, return browser.tabs.New().""" 247 return browser.tabs[0] 248 249 def ValidatePageSet(self, page_set): 250 """Override to examine the page set before the test run. Useful for 251 example to validate that the pageset can be used with the test.""" 252 253 def ValidateAndMeasurePage(self, page, tab, results): 254 """Override to check test assertions and perform measurement. 255 256 When adding measurement results, call results.AddValue(...) for 257 each result. Raise an exception or add a failure.FailureValue on 258 failure. page_test.py also provides several base exception classes 259 to use. 260 261 Prefer metric value names that are in accordance with python 262 variable style. e.g., metric_name. The name 'url' must not be used. 263 264 Put together: 265 def ValidateAndMeasurePage(self, page, tab, results): 266 res = tab.EvaluateJavaScript('2+2') 267 if res != 4: 268 raise Exception('Oh, wow.') 269 results.AddValue(scalar.ScalarValue( 270 page, 'two_plus_two', 'count', res)) 271 272 Args: 273 page: A telemetry.page.Page instance. 274 tab: A telemetry.core.Tab instance. 275 results: A telemetry.results.PageTestResults instance. 276 """ 277 # TODO(chrishenry): Switch to raise NotImplementedError() when 278 # subclasses no longer override ValidatePage/MeasurePage. 279 self.ValidatePage(page, tab, results) 280 281 def ValidatePage(self, page, tab, results): 282 """DEPRECATED: Use ValidateAndMeasurePage instead.""" 283 self.MeasurePage(page, tab, results) 284 285 def MeasurePage(self, page, tab, results): 286 """DEPRECATED: Use ValidateAndMeasurePage instead.""" 287 288 def RunPage(self, page, tab, results): 289 # Run actions. 290 interactive = self.options and self.options.interactive 291 action_runner = action_runner_module.ActionRunner( 292 tab, skip_waits=page.skip_waits) 293 self.WillRunActions(page, tab) 294 if interactive: 295 action_runner.PauseInteractive() 296 else: 297 self._RunMethod(page, self._action_name_to_run, action_runner) 298 self.DidRunActions(page, tab) 299 300 self.ValidateAndMeasurePage(page, tab, results) 301 302 def _RunMethod(self, page, method_name, action_runner): 303 if hasattr(page, method_name): 304 run_method = getattr(page, method_name) 305 run_method(action_runner) 306 307 def RunNavigateSteps(self, page, tab): 308 """Navigates the tab to the page URL attribute. 309 310 Runs the 'navigate_steps' page attribute as a compound action. 311 """ 312 action_runner = action_runner_module.ActionRunner( 313 tab, skip_waits=page.skip_waits) 314 page.RunNavigateSteps(action_runner) 315 316 def IsExiting(self): 317 return self._exit_requested 318 319 def RequestExit(self): 320 self._exit_requested = True 321 322 @property 323 def action_name_to_run(self): 324 return self._action_name_to_run 325