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