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 if not hasattr(func, '_disabled_strings'): 113 func._disabled_strings = set() 114 func._disabled_strings.update(disabled_strings) 115 return func 116 assert args, ( 117 "@Disabled(...) requires arguments. Use @Disabled('all') if you want to " 118 'unconditionally disable the test.') 119 assert not callable(args[0]), 'Please use @Disabled(..).' 120 disabled_strings = list(args) 121 for disabled_string in disabled_strings: 122 # TODO(tonyg): Validate that these strings are recognized. 123 assert isinstance(disabled_string, str), '@Disabled accepts a list of strs' 124 return _Disabled 125 126 127def Enabled(*args): 128 """Decorator for enabling tests/benchmarks. 129 130 The test will be enabled if ANY of the args match the browser type, OS name 131 or OS version: 132 @Enabled('canary') # Enabled only for canary browsers 133 @Enabled('win') # Enabled only on Windows. 134 @Enabled('win', 'linux') # Enabled only on Windows or Linux. 135 @Enabled('mavericks') # Enabled only on Mac Mavericks (10.9). 136 """ 137 def _Enabled(func): 138 if not hasattr(func, '_enabled_strings'): 139 func._enabled_strings = set() 140 func._enabled_strings.update(enabled_strings) 141 return func 142 assert args, '@Enabled(..) requires arguments' 143 assert not callable(args[0]), 'Please use @Enabled(..).' 144 enabled_strings = list(args) 145 for enabled_string in enabled_strings: 146 # TODO(tonyg): Validate that these strings are recognized. 147 assert isinstance(enabled_string, str), '@Enabled accepts a list of strs' 148 return _Enabled 149 150 151# TODO(dpranke): Remove if we don't need this. 152def Isolated(*args): 153 """Decorator for noting that tests must be run in isolation. 154 155 The test will be run by itself (not concurrently with any other tests) 156 if ANY of the args match the browser type, OS name, or OS version.""" 157 def _Isolated(func): 158 if not isinstance(func, types.FunctionType): 159 func._isolated_strings = isolated_strings 160 return func 161 @functools.wraps(func) 162 def wrapper(*args, **kwargs): 163 func(*args, **kwargs) 164 wrapper._isolated_strings = isolated_strings 165 return wrapper 166 if len(args) == 1 and callable(args[0]): 167 isolated_strings = [] 168 return _Isolated(args[0]) 169 isolated_strings = list(args) 170 for isolated_string in isolated_strings: 171 # TODO(tonyg): Validate that these strings are recognized. 172 assert isinstance(isolated_string, str), 'Isolated accepts a list of strs' 173 return _Isolated 174 175 176# TODO(nednguyen): Remove this and have call site just use ShouldSkip directly. 177def IsEnabled(test, possible_browser): 178 """Returns True iff |test| is enabled given the |possible_browser|. 179 180 Use to respect the @Enabled / @Disabled decorators. 181 182 Args: 183 test: A function or class that may contain _disabled_strings and/or 184 _enabled_strings attributes. 185 possible_browser: A PossibleBrowser to check whether |test| may run against. 186 """ 187 should_skip, msg = ShouldSkip(test, possible_browser) 188 return (not should_skip, msg) 189 190 191def ShouldSkip(test, possible_browser): 192 """Returns whether the test should be skipped and the reason for it.""" 193 platform_attributes = _PlatformAttributes(possible_browser) 194 195 if hasattr(test, '__name__'): 196 name = test.__name__ 197 elif hasattr(test, '__class__'): 198 name = test.__class__.__name__ 199 else: 200 name = str(test) 201 202 skip = 'Skipping %s (%s) because' % (name, str(test)) 203 running = 'You are running %r.' % platform_attributes 204 205 if hasattr(test, '_disabled_strings'): 206 if 'all' in test._disabled_strings: 207 return (True, '%s it is unconditionally disabled.' % skip) 208 if set(test._disabled_strings) & set(platform_attributes): 209 return (True, '%s it is disabled for %s. %s' % 210 (skip, ' and '.join(test._disabled_strings), running)) 211 212 if hasattr(test, '_enabled_strings'): 213 if 'all' in test._enabled_strings: 214 return False, None # No arguments to @Enabled means always enable. 215 if not set(test._enabled_strings) & set(platform_attributes): 216 return (True, '%s it is only enabled for %s. %s' % 217 (skip, ' or '.join(test._enabled_strings), running)) 218 219 return False, None 220 221 222def ShouldBeIsolated(test, possible_browser): 223 platform_attributes = _PlatformAttributes(possible_browser) 224 if hasattr(test, '_isolated_strings'): 225 isolated_strings = test._isolated_strings 226 if not isolated_strings: 227 return True # No arguments to @Isolated means always isolate. 228 for isolated_string in isolated_strings: 229 if isolated_string in platform_attributes: 230 return True 231 return False 232 return False 233 234 235def _PlatformAttributes(possible_browser): 236 """Returns a list of platform attribute strings.""" 237 attributes = [a.lower() for a in [ 238 possible_browser.browser_type, 239 possible_browser.platform.GetOSName(), 240 possible_browser.platform.GetOSVersionName(), 241 ]] 242 if possible_browser.supports_tab_control: 243 attributes.append('has tabs') 244 if 'content-shell' in possible_browser.browser_type: 245 attributes.append('content-shell') 246 if possible_browser.browser_type == 'reference': 247 ref_attributes = [] 248 for attribute in attributes: 249 if attribute != 'reference': 250 ref_attributes.append('%s-reference' % attribute) 251 attributes.extend(ref_attributes) 252 return attributes 253