1# Copyright 2014 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# pylint: disable=protected-access 5 6import datetime 7import functools 8import os 9import inspect 10import types 11import warnings 12 13 14def Cache(obj): 15 """Decorator for caching read-only properties. 16 17 Example usage (always returns the same Foo instance): 18 @Cache 19 def CreateFoo(): 20 return Foo() 21 22 If CreateFoo() accepts parameters, a separate cached value is maintained 23 for each unique parameter combination. 24 25 Cached methods maintain their cache for the lifetime of the /instance/, while 26 cached functions maintain their cache for the lifetime of the /module/. 27 """ 28 29 @functools.wraps(obj) 30 def Cacher(*args, **kwargs): 31 cacher = args[0] if inspect.getargspec(obj).args[:1] == ['self'] else obj 32 cacher.__cache = cacher.__cache if hasattr(cacher, '__cache') else {} 33 key = str(obj) + str(args) + str(kwargs) 34 if key not in cacher.__cache: 35 cacher.__cache[key] = obj(*args, **kwargs) 36 return cacher.__cache[key] 37 38 return Cacher 39 40 41class Deprecated(object): 42 43 def __init__(self, year, month, day, extra_guidance=''): 44 self._date_of_support_removal = datetime.date(year, month, day) 45 self._extra_guidance = extra_guidance 46 47 def _DisplayWarningMessage(self, target): 48 target_str = '' 49 if isinstance(target, types.FunctionType): 50 target_str = 'Function %s' % target.__name__ 51 else: 52 target_str = 'Class %s' % target.__name__ 53 warnings.warn( 54 '%s is deprecated. It will no longer be supported on %s. ' 55 'Please remove it or switch to an alternative before ' 56 'that time. %s\n' % 57 (target_str, self._date_of_support_removal.strftime('%B %d, %Y'), 58 self._extra_guidance), 59 stacklevel=self._ComputeStackLevel()) 60 61 def _ComputeStackLevel(self): 62 this_file, _ = os.path.splitext(__file__) 63 frame = inspect.currentframe() 64 i = 0 65 while True: 66 filename = frame.f_code.co_filename 67 if not filename.startswith(this_file): 68 return i 69 frame = frame.f_back 70 i += 1 71 72 def __call__(self, target): 73 if isinstance(target, types.FunctionType): 74 75 @functools.wraps(target) 76 def wrapper(*args, **kwargs): # pylint: disable=invalid-name 77 self._DisplayWarningMessage(target) 78 return target(*args, **kwargs) 79 80 return wrapper 81 elif inspect.isclass(target): 82 original_ctor = target.__init__ 83 84 # We have to handle case original_ctor is object.__init__ separately 85 # since object.__init__ does not have __module__ defined, which 86 # cause functools.wraps() to raise exception. 87 if original_ctor == object.__init__: 88 89 def new_ctor(*args, **kwargs): # pylint: disable=invalid-name 90 self._DisplayWarningMessage(target) 91 return original_ctor(*args, **kwargs) 92 else: 93 94 @functools.wraps(original_ctor) 95 def new_ctor(*args, **kwargs): # pylint: disable=invalid-name 96 self._DisplayWarningMessage(target) 97 return original_ctor(*args, **kwargs) 98 99 target.__init__ = new_ctor 100 return target 101 else: 102 raise TypeError('@Deprecated is only applicable to functions or classes') 103 104 105def Disabled(*args): 106 """Decorator for disabling tests/benchmarks. 107 108 If args are given, the test will be disabled if ANY of the args match the 109 browser type, OS name, OS version, or any tags returned by a PossibleBrowser's 110 GetTypExpectationsTags(): 111 @Disabled('canary') # Disabled for canary browsers 112 @Disabled('win') # Disabled on Windows. 113 @Disabled('win', 'linux') # Disabled on both Windows and Linux. 114 @Disabled('mavericks') # Disabled on Mac Mavericks (10.9) only. 115 @Disabled('all') # Unconditionally disabled. 116 @Disabled('chromeos-local') # Disabled in ChromeOS local mode. 117 """ 118 119 def _Disabled(func): 120 if inspect.isclass(func): 121 raise TypeError('Decorators cannot disable classes. ' 122 'You need to place them on the test methods instead.') 123 disabled_attr_name = DisabledAttributeName(func) 124 if not hasattr(func, disabled_attr_name): 125 setattr(func, disabled_attr_name, set()) 126 disabled_set = getattr(func, disabled_attr_name) 127 disabled_set.update(disabled_strings) 128 setattr(func, disabled_attr_name, disabled_set) 129 return func 130 131 assert args, ( 132 "@Disabled(...) requires arguments. Use @Disabled('all') if you want to " 133 'unconditionally disable the test.') 134 assert not callable(args[0]), 'Please use @Disabled(..).' 135 disabled_strings = list(args) 136 for disabled_string in disabled_strings: 137 # TODO(tonyg): Validate that these strings are recognized. 138 assert isinstance(disabled_string, str), '@Disabled accepts a list of strs' 139 return _Disabled 140 141 142def Enabled(*args): 143 """Decorator for enabling tests/benchmarks. 144 145 The test will be enabled if ANY of the args match the browser type, OS name, 146 OS version, or any tags returned by a PossibleBrowser's 147 GetTypExpectationsTags(): 148 @Enabled('canary') # Enabled only for canary browsers 149 @Enabled('win') # Enabled only on Windows. 150 @Enabled('win', 'linux') # Enabled only on Windows or Linux. 151 @Enabled('mavericks') # Enabled only on Mac Mavericks (10.9). 152 @Enabled('chromeos-local') # Enabled only in ChromeOS local mode. 153 """ 154 155 def _Enabled(func): 156 if inspect.isclass(func): 157 raise TypeError('Decorators cannot enable classes. ' 158 'You need to place them on the test methods instead.') 159 enabled_attr_name = EnabledAttributeName(func) 160 if not hasattr(func, enabled_attr_name): 161 setattr(func, enabled_attr_name, set()) 162 enabled_set = getattr(func, enabled_attr_name) 163 enabled_set.update(enabled_strings) 164 setattr(func, enabled_attr_name, enabled_set) 165 return func 166 167 assert args, '@Enabled(..) requires arguments' 168 assert not callable(args[0]), 'Please use @Enabled(..).' 169 enabled_strings = list(args) 170 for enabled_string in enabled_strings: 171 # TODO(tonyg): Validate that these strings are recognized. 172 assert isinstance(enabled_string, str), '@Enabled accepts a list of strs' 173 return _Enabled 174 175 176def Info(emails=None, component=None, documentation_url=None, info_blurb=None): 177 """Decorator for specifying the benchmark_info of a benchmark.""" 178 179 def _Info(func): 180 info_attr_name = InfoAttributeName(func) 181 assert inspect.isclass(func), '@Info(...) can only be used on classes' 182 if not hasattr(func, info_attr_name): 183 setattr(func, info_attr_name, {}) 184 info_dict = getattr(func, info_attr_name) 185 if emails: 186 assert 'emails' not in info_dict, 'emails can only be set once' 187 info_dict['emails'] = emails 188 if component: 189 assert 'component' not in info_dict, 'component can only be set once' 190 info_dict['component'] = component 191 if documentation_url: 192 assert 'documentation_url' not in info_dict, ( 193 'document link can only be set once') 194 info_dict['documentation_url'] = documentation_url 195 if info_blurb: 196 assert 'info_blurb' not in info_dict, ( 197 'info_blurb can only be set once') 198 info_dict['info_blurb'] = info_blurb 199 200 setattr(func, info_attr_name, info_dict) 201 return func 202 203 help_text = '@Info(...) requires emails and/or a component' 204 assert emails or component, help_text 205 if emails: 206 assert isinstance(emails, list), 'emails must be a list of strs' 207 for e in emails: 208 assert isinstance(e, str), 'emails must be a list of strs' 209 if documentation_url: 210 assert isinstance(documentation_url, str), ( 211 'Documentation link must be a str') 212 assert (documentation_url.startswith('http://') or 213 documentation_url.startswith('https://')), ( 214 'Documentation url is malformed') 215 if info_blurb: 216 assert isinstance(info_blurb, str), ('info_blurb must be a str') 217 return _Info 218 219 220# TODO(dpranke): Remove if we don't need this. 221def Isolated(*args): 222 """Decorator for noting that tests must be run in isolation. 223 224 The test will be run by itself (not concurrently with any other tests) 225 if ANY of the args match the browser type, OS name, or OS version.""" 226 227 def _Isolated(func): 228 if not isinstance(func, types.FunctionType): 229 func._isolated_strings = isolated_strings 230 return func 231 232 @functools.wraps(func) 233 def wrapper(*args, **kwargs): # pylint: disable=invalid-name 234 func(*args, **kwargs) 235 236 wrapper._isolated_strings = isolated_strings 237 return wrapper 238 239 if len(args) == 1 and callable(args[0]): 240 isolated_strings = [] 241 return _Isolated(args[0]) 242 isolated_strings = list(args) 243 for isolated_string in isolated_strings: 244 # TODO(tonyg): Validate that these strings are recognized. 245 assert isinstance(isolated_string, str), 'Isolated accepts a list of strs' 246 return _Isolated 247 248 249# TODO(crbug.com/1111556): Remove this and have call site just use ShouldSkip 250# directly. 251def IsEnabled(test, possible_browser): 252 """Returns True iff |test| is enabled given the |possible_browser|. 253 254 Use to respect the @Enabled / @Disabled decorators. 255 256 Args: 257 test: A function or class that may contain _disabled_strings and/or 258 _enabled_strings attributes. 259 possible_browser: A PossibleBrowser to check whether |test| may run against. 260 """ 261 should_skip, msg = ShouldSkip(test, possible_browser) 262 return (not should_skip, msg) 263 264 265def _TestName(test): 266 if inspect.ismethod(test): 267 # On methods, __name__ is "instancemethod", use __func__.__name__ instead. 268 test = test.__func__ 269 if hasattr(test, '__name__'): 270 return test.__name__ 271 elif hasattr(test, '__class__'): 272 return test.__class__.__name__ 273 return str(test) 274 275 276def DisabledAttributeName(test): 277 name = _TestName(test) 278 return '_%s_%s_disabled_strings' % (test.__module__, name) 279 280 281def GetDisabledAttributes(test): 282 disabled_attr_name = DisabledAttributeName(test) 283 if not hasattr(test, disabled_attr_name): 284 return set() 285 return set(getattr(test, disabled_attr_name)) 286 287 288def GetEnabledAttributes(test): 289 enabled_attr_name = EnabledAttributeName(test) 290 if not hasattr(test, enabled_attr_name): 291 return set() 292 enabled_strings = set(getattr(test, enabled_attr_name)) 293 return enabled_strings 294 295 296def EnabledAttributeName(test): 297 name = _TestName(test) 298 return '_%s_%s_enabled_strings' % (test.__module__, name) 299 300 301def InfoAttributeName(test): 302 name = _TestName(test) 303 return '_%s_%s_info' % (test.__module__, name) 304 305 306def GetEmails(test): 307 info_attr_name = InfoAttributeName(test) 308 benchmark_info = getattr(test, info_attr_name, {}) 309 if 'emails' in benchmark_info: 310 return benchmark_info['emails'] 311 return None 312 313 314def GetComponent(test): 315 info_attr_name = InfoAttributeName(test) 316 benchmark_info = getattr(test, info_attr_name, {}) 317 if 'component' in benchmark_info: 318 return benchmark_info['component'] 319 return None 320 321 322def GetDocumentationLink(test): 323 info_attr_name = InfoAttributeName(test) 324 benchmark_info = getattr(test, info_attr_name, {}) 325 if 'documentation_url' in benchmark_info: 326 return benchmark_info['documentation_url'] 327 return None 328 329def GetInfoBlurb(test): 330 info_attr_name = InfoAttributeName(test) 331 benchmark_info = getattr(test, info_attr_name, {}) 332 if 'info_blurb' in benchmark_info: 333 return benchmark_info['info_blurb'] 334 return None 335 336 337def ShouldSkip(test, possible_browser): 338 """Returns whether the test should be skipped and the reason for it.""" 339 platform_attributes = _PlatformAttributes(possible_browser) 340 341 name = _TestName(test) 342 skip = 'Skipping %s (%s) because' % (name, str(test)) 343 running = 'You are running %r.' % platform_attributes 344 345 disabled_attr_name = DisabledAttributeName(test) 346 if hasattr(test, disabled_attr_name): 347 disabled_strings = getattr(test, disabled_attr_name) 348 if 'all' in disabled_strings: 349 return (True, '%s it is unconditionally disabled.' % skip) 350 if set(disabled_strings) & set(platform_attributes): 351 return (True, '%s it is disabled for %s. %s' % 352 (skip, ' and '.join(disabled_strings), running)) 353 354 enabled_attr_name = EnabledAttributeName(test) 355 if hasattr(test, enabled_attr_name): 356 enabled_strings = getattr(test, enabled_attr_name) 357 if 'all' in enabled_strings: 358 return False, None # No arguments to @Enabled means always enable. 359 if not set(enabled_strings) & set(platform_attributes): 360 return (True, '%s it is only enabled for %s. %s' % 361 (skip, ' or '.join(enabled_strings), running)) 362 363 return False, None 364 365 366def ShouldBeIsolated(test, possible_browser): 367 platform_attributes = _PlatformAttributes(possible_browser) 368 if hasattr(test, '_isolated_strings'): 369 isolated_strings = test._isolated_strings 370 if not isolated_strings: 371 return True # No arguments to @Isolated means always isolate. 372 for isolated_string in isolated_strings: 373 if isolated_string in platform_attributes: 374 return True 375 return False 376 return False 377 378 379def _PlatformAttributes(possible_browser): 380 """Returns a list of platform attribute strings.""" 381 attributes = [ 382 a.lower() 383 for a in [ 384 possible_browser.browser_type, 385 possible_browser.platform.GetOSName(), 386 possible_browser.platform.GetOSVersionName(), 387 ] 388 ] 389 if possible_browser.supports_tab_control: 390 attributes.append('has tabs') 391 if 'content-shell' in possible_browser.browser_type: 392 attributes.append('content-shell') 393 if possible_browser.browser_type == 'reference': 394 ref_attributes = [] 395 for attribute in attributes: 396 if attribute != 'reference': 397 ref_attributes.append('%s-reference' % attribute) 398 attributes.extend(ref_attributes) 399 attributes.extend(possible_browser.GetTypExpectationsTags()) 400 return attributes 401