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