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