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