| # Copyright 2014 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| # pylint: disable=protected-access |
| |
| import datetime |
| import functools |
| import os |
| import inspect |
| import types |
| import warnings |
| |
| |
| def Cache(obj): |
| """Decorator for caching read-only properties. |
| |
| Example usage (always returns the same Foo instance): |
| @Cache |
| def CreateFoo(): |
| return Foo() |
| |
| If CreateFoo() accepts parameters, a separate cached value is maintained |
| for each unique parameter combination. |
| |
| Cached methods maintain their cache for the lifetime of the /instance/, while |
| cached functions maintain their cache for the lifetime of the /module/. |
| """ |
| @functools.wraps(obj) |
| def Cacher(*args, **kwargs): |
| cacher = args[0] if inspect.getargspec(obj).args[:1] == ['self'] else obj |
| cacher.__cache = cacher.__cache if hasattr(cacher, '__cache') else {} |
| key = str(obj) + str(args) + str(kwargs) |
| if key not in cacher.__cache: |
| cacher.__cache[key] = obj(*args, **kwargs) |
| return cacher.__cache[key] |
| return Cacher |
| |
| |
| class Deprecated(object): |
| |
| def __init__(self, year, month, day, extra_guidance=''): |
| self._date_of_support_removal = datetime.date(year, month, day) |
| self._extra_guidance = extra_guidance |
| |
| def _DisplayWarningMessage(self, target): |
| target_str = '' |
| if isinstance(target, types.FunctionType): |
| target_str = 'Function %s' % target.__name__ |
| else: |
| target_str = 'Class %s' % target.__name__ |
| warnings.warn('%s is deprecated. It will no longer be supported on %s. ' |
| 'Please remove it or switch to an alternative before ' |
| 'that time. %s\n' |
| % (target_str, |
| self._date_of_support_removal.strftime('%B %d, %Y'), |
| self._extra_guidance), |
| stacklevel=self._ComputeStackLevel()) |
| |
| def _ComputeStackLevel(self): |
| this_file, _ = os.path.splitext(__file__) |
| frame = inspect.currentframe() |
| i = 0 |
| while True: |
| filename = frame.f_code.co_filename |
| if not filename.startswith(this_file): |
| return i |
| frame = frame.f_back |
| i += 1 |
| |
| def __call__(self, target): |
| if isinstance(target, types.FunctionType): |
| @functools.wraps(target) |
| def wrapper(*args, **kwargs): |
| self._DisplayWarningMessage(target) |
| return target(*args, **kwargs) |
| return wrapper |
| elif inspect.isclass(target): |
| original_ctor = target.__init__ |
| |
| # We have to handle case original_ctor is object.__init__ separately |
| # since object.__init__ does not have __module__ defined, which |
| # cause functools.wraps() to raise exception. |
| if original_ctor == object.__init__: |
| def new_ctor(*args, **kwargs): |
| self._DisplayWarningMessage(target) |
| return original_ctor(*args, **kwargs) |
| else: |
| @functools.wraps(original_ctor) |
| def new_ctor(*args, **kwargs): |
| self._DisplayWarningMessage(target) |
| return original_ctor(*args, **kwargs) |
| |
| target.__init__ = new_ctor |
| return target |
| else: |
| raise TypeError('@Deprecated is only applicable to functions or classes') |
| |
| |
| def Disabled(*args): |
| """Decorator for disabling tests/benchmarks. |
| |
| |
| If args are given, the test will be disabled if ANY of the args match the |
| browser type, OS name or OS version: |
| @Disabled('canary') # Disabled for canary browsers |
| @Disabled('win') # Disabled on Windows. |
| @Disabled('win', 'linux') # Disabled on both Windows and Linux. |
| @Disabled('mavericks') # Disabled on Mac Mavericks (10.9) only. |
| @Disabled('all') # Unconditionally disabled. |
| """ |
| def _Disabled(func): |
| disabled_attr_name = DisabledAttributeName(func) |
| if not hasattr(func, disabled_attr_name): |
| setattr(func, disabled_attr_name, set()) |
| disabled_set = getattr(func, disabled_attr_name) |
| disabled_set.update(disabled_strings) |
| setattr(func, disabled_attr_name, disabled_set) |
| return func |
| assert args, ( |
| "@Disabled(...) requires arguments. Use @Disabled('all') if you want to " |
| 'unconditionally disable the test.') |
| assert not callable(args[0]), 'Please use @Disabled(..).' |
| disabled_strings = list(args) |
| for disabled_string in disabled_strings: |
| # TODO(tonyg): Validate that these strings are recognized. |
| assert isinstance(disabled_string, str), '@Disabled accepts a list of strs' |
| return _Disabled |
| |
| |
| def Enabled(*args): |
| """Decorator for enabling tests/benchmarks. |
| |
| The test will be enabled if ANY of the args match the browser type, OS name |
| or OS version: |
| @Enabled('canary') # Enabled only for canary browsers |
| @Enabled('win') # Enabled only on Windows. |
| @Enabled('win', 'linux') # Enabled only on Windows or Linux. |
| @Enabled('mavericks') # Enabled only on Mac Mavericks (10.9). |
| """ |
| def _Enabled(func): |
| enabled_attr_name = EnabledAttributeName(func) |
| if not hasattr(func, enabled_attr_name): |
| setattr(func, enabled_attr_name, set()) |
| enabled_set = getattr(func, enabled_attr_name) |
| enabled_set.update(enabled_strings) |
| setattr(func, enabled_attr_name, enabled_set) |
| return func |
| assert args, '@Enabled(..) requires arguments' |
| assert not callable(args[0]), 'Please use @Enabled(..).' |
| enabled_strings = list(args) |
| for enabled_string in enabled_strings: |
| # TODO(tonyg): Validate that these strings are recognized. |
| assert isinstance(enabled_string, str), '@Enabled accepts a list of strs' |
| return _Enabled |
| |
| |
| # TODO(dpranke): Remove if we don't need this. |
| def Isolated(*args): |
| """Decorator for noting that tests must be run in isolation. |
| |
| The test will be run by itself (not concurrently with any other tests) |
| if ANY of the args match the browser type, OS name, or OS version.""" |
| def _Isolated(func): |
| if not isinstance(func, types.FunctionType): |
| func._isolated_strings = isolated_strings |
| return func |
| @functools.wraps(func) |
| def wrapper(*args, **kwargs): |
| func(*args, **kwargs) |
| wrapper._isolated_strings = isolated_strings |
| return wrapper |
| if len(args) == 1 and callable(args[0]): |
| isolated_strings = [] |
| return _Isolated(args[0]) |
| isolated_strings = list(args) |
| for isolated_string in isolated_strings: |
| # TODO(tonyg): Validate that these strings are recognized. |
| assert isinstance(isolated_string, str), 'Isolated accepts a list of strs' |
| return _Isolated |
| |
| |
| # TODO(nednguyen): Remove this and have call site just use ShouldSkip directly. |
| def IsEnabled(test, possible_browser): |
| """Returns True iff |test| is enabled given the |possible_browser|. |
| |
| Use to respect the @Enabled / @Disabled decorators. |
| |
| Args: |
| test: A function or class that may contain _disabled_strings and/or |
| _enabled_strings attributes. |
| possible_browser: A PossibleBrowser to check whether |test| may run against. |
| """ |
| should_skip, msg = ShouldSkip(test, possible_browser) |
| return (not should_skip, msg) |
| |
| |
| def _TestName(test): |
| if inspect.ismethod(test): |
| # On methods, __name__ is "instancemethod", use __func__.__name__ instead. |
| test = test.__func__ |
| if hasattr(test, '__name__'): |
| return test.__name__ |
| elif hasattr(test, '__class__'): |
| return test.__class__.__name__ |
| return str(test) |
| |
| |
| def DisabledAttributeName(test): |
| name = _TestName(test) |
| return '_%s_%s_disabled_strings' % (test.__module__, name) |
| |
| |
| def GetDisabledAttributes(test): |
| disabled_attr_name = DisabledAttributeName(test) |
| if not hasattr(test, disabled_attr_name): |
| return set() |
| return set(getattr(test, disabled_attr_name)) |
| |
| |
| def GetEnabledAttributes(test): |
| enabled_attr_name = EnabledAttributeName(test) |
| if not hasattr(test, enabled_attr_name): |
| return set() |
| enabled_strings = set(getattr(test, enabled_attr_name)) |
| return enabled_strings |
| |
| |
| def EnabledAttributeName(test): |
| name = _TestName(test) |
| return '_%s_%s_enabled_strings' % (test.__module__, name) |
| |
| |
| def ShouldSkip(test, possible_browser): |
| """Returns whether the test should be skipped and the reason for it.""" |
| platform_attributes = _PlatformAttributes(possible_browser) |
| |
| name = _TestName(test) |
| skip = 'Skipping %s (%s) because' % (name, str(test)) |
| running = 'You are running %r.' % platform_attributes |
| |
| disabled_attr_name = DisabledAttributeName(test) |
| if hasattr(test, disabled_attr_name): |
| disabled_strings = getattr(test, disabled_attr_name) |
| if 'all' in disabled_strings: |
| return (True, '%s it is unconditionally disabled.' % skip) |
| if set(disabled_strings) & set(platform_attributes): |
| return (True, '%s it is disabled for %s. %s' % |
| (skip, ' and '.join(disabled_strings), running)) |
| |
| enabled_attr_name = EnabledAttributeName(test) |
| if hasattr(test, enabled_attr_name): |
| enabled_strings = getattr(test, enabled_attr_name) |
| if 'all' in enabled_strings: |
| return False, None # No arguments to @Enabled means always enable. |
| if not set(enabled_strings) & set(platform_attributes): |
| return (True, '%s it is only enabled for %s. %s' % |
| (skip, ' or '.join(enabled_strings), running)) |
| |
| return False, None |
| |
| |
| def ShouldBeIsolated(test, possible_browser): |
| platform_attributes = _PlatformAttributes(possible_browser) |
| if hasattr(test, '_isolated_strings'): |
| isolated_strings = test._isolated_strings |
| if not isolated_strings: |
| return True # No arguments to @Isolated means always isolate. |
| for isolated_string in isolated_strings: |
| if isolated_string in platform_attributes: |
| return True |
| return False |
| return False |
| |
| |
| def _PlatformAttributes(possible_browser): |
| """Returns a list of platform attribute strings.""" |
| attributes = [a.lower() for a in [ |
| possible_browser.browser_type, |
| possible_browser.platform.GetOSName(), |
| possible_browser.platform.GetOSVersionName(), |
| ]] |
| if possible_browser.supports_tab_control: |
| attributes.append('has tabs') |
| if 'content-shell' in possible_browser.browser_type: |
| attributes.append('content-shell') |
| if possible_browser.browser_type == 'reference': |
| ref_attributes = [] |
| for attribute in attributes: |
| if attribute != 'reference': |
| ref_attributes.append('%s-reference' % attribute) |
| attributes.extend(ref_attributes) |
| return attributes |