1#! /usr/bin/env python
2#
3# Protocol Buffers - Google's data interchange format
4# Copyright 2008 Google Inc.  All rights reserved.
5# https://developers.google.com/protocol-buffers/
6#
7# Redistribution and use in source and binary forms, with or without
8# modification, are permitted provided that the following conditions are
9# met:
10#
11#     * Redistributions of source code must retain the above copyright
12# notice, this list of conditions and the following disclaimer.
13#     * Redistributions in binary form must reproduce the above
14# copyright notice, this list of conditions and the following disclaimer
15# in the documentation and/or other materials provided with the
16# distribution.
17#     * Neither the name of Google Inc. nor the names of its
18# contributors may be used to endorse or promote products derived from
19# this software without specific prior written permission.
20#
21# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
24# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
25# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
28# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
29# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
33"""Adds support for parameterized tests to Python's unittest TestCase class.
34
35A parameterized test is a method in a test case that is invoked with different
36argument tuples.
37
38A simple example:
39
40  class AdditionExample(parameterized.TestCase):
41    @parameterized.parameters(
42       (1, 2, 3),
43       (4, 5, 9),
44       (1, 1, 3))
45    def testAddition(self, op1, op2, result):
46      self.assertEqual(result, op1 + op2)
47
48
49Each invocation is a separate test case and properly isolated just
50like a normal test method, with its own setUp/tearDown cycle. In the
51example above, there are three separate testcases, one of which will
52fail due to an assertion error (1 + 1 != 3).
53
54Parameters for invididual test cases can be tuples (with positional parameters)
55or dictionaries (with named parameters):
56
57  class AdditionExample(parameterized.TestCase):
58    @parameterized.parameters(
59       {'op1': 1, 'op2': 2, 'result': 3},
60       {'op1': 4, 'op2': 5, 'result': 9},
61    )
62    def testAddition(self, op1, op2, result):
63      self.assertEqual(result, op1 + op2)
64
65If a parameterized test fails, the error message will show the
66original test name (which is modified internally) and the arguments
67for the specific invocation, which are part of the string returned by
68the shortDescription() method on test cases.
69
70The id method of the test, used internally by the unittest framework,
71is also modified to show the arguments. To make sure that test names
72stay the same across several invocations, object representations like
73
74  >>> class Foo(object):
75  ...  pass
76  >>> repr(Foo())
77  '<__main__.Foo object at 0x23d8610>'
78
79are turned into '<__main__.Foo>'. For even more descriptive names,
80especially in test logs, you can use the named_parameters decorator. In
81this case, only tuples are supported, and the first parameters has to
82be a string (or an object that returns an apt name when converted via
83str()):
84
85  class NamedExample(parameterized.TestCase):
86    @parameterized.named_parameters(
87       ('Normal', 'aa', 'aaa', True),
88       ('EmptyPrefix', '', 'abc', True),
89       ('BothEmpty', '', '', True))
90    def testStartsWith(self, prefix, string, result):
91      self.assertEqual(result, strings.startswith(prefix))
92
93Named tests also have the benefit that they can be run individually
94from the command line:
95
96  $ testmodule.py NamedExample.testStartsWithNormal
97  .
98  --------------------------------------------------------------------
99  Ran 1 test in 0.000s
100
101  OK
102
103Parameterized Classes
104=====================
105If invocation arguments are shared across test methods in a single
106TestCase class, instead of decorating all test methods
107individually, the class itself can be decorated:
108
109  @parameterized.parameters(
110    (1, 2, 3)
111    (4, 5, 9))
112  class ArithmeticTest(parameterized.TestCase):
113    def testAdd(self, arg1, arg2, result):
114      self.assertEqual(arg1 + arg2, result)
115
116    def testSubtract(self, arg2, arg2, result):
117      self.assertEqual(result - arg1, arg2)
118
119Inputs from Iterables
120=====================
121If parameters should be shared across several test cases, or are dynamically
122created from other sources, a single non-tuple iterable can be passed into
123the decorator. This iterable will be used to obtain the test cases:
124
125  class AdditionExample(parameterized.TestCase):
126    @parameterized.parameters(
127      c.op1, c.op2, c.result for c in testcases
128    )
129    def testAddition(self, op1, op2, result):
130      self.assertEqual(result, op1 + op2)
131
132
133Single-Argument Test Methods
134============================
135If a test method takes only one argument, the single argument does not need to
136be wrapped into a tuple:
137
138  class NegativeNumberExample(parameterized.TestCase):
139    @parameterized.parameters(
140       -1, -3, -4, -5
141    )
142    def testIsNegative(self, arg):
143      self.assertTrue(IsNegative(arg))
144"""
145
146__author__ = 'tmarek@google.com (Torsten Marek)'
147
148import functools
149import re
150import types
151try:
152  import unittest2 as unittest
153except ImportError:
154  import unittest
155import uuid
156
157import six
158
159try:
160  # Since python 3
161  import collections.abc as collections_abc
162except ImportError:
163  # Won't work after python 3.8
164  import collections as collections_abc
165
166ADDR_RE = re.compile(r'\<([a-zA-Z0-9_\-\.]+) object at 0x[a-fA-F0-9]+\>')
167_SEPARATOR = uuid.uuid1().hex
168_FIRST_ARG = object()
169_ARGUMENT_REPR = object()
170
171
172def _CleanRepr(obj):
173  return ADDR_RE.sub(r'<\1>', repr(obj))
174
175
176# Helper function formerly from the unittest module, removed from it in
177# Python 2.7.
178def _StrClass(cls):
179  return '%s.%s' % (cls.__module__, cls.__name__)
180
181
182def _NonStringIterable(obj):
183  return (isinstance(obj, collections_abc.Iterable) and not
184          isinstance(obj, six.string_types))
185
186
187def _FormatParameterList(testcase_params):
188  if isinstance(testcase_params, collections_abc.Mapping):
189    return ', '.join('%s=%s' % (argname, _CleanRepr(value))
190                     for argname, value in testcase_params.items())
191  elif _NonStringIterable(testcase_params):
192    return ', '.join(map(_CleanRepr, testcase_params))
193  else:
194    return _FormatParameterList((testcase_params,))
195
196
197class _ParameterizedTestIter(object):
198  """Callable and iterable class for producing new test cases."""
199
200  def __init__(self, test_method, testcases, naming_type):
201    """Returns concrete test functions for a test and a list of parameters.
202
203    The naming_type is used to determine the name of the concrete
204    functions as reported by the unittest framework. If naming_type is
205    _FIRST_ARG, the testcases must be tuples, and the first element must
206    have a string representation that is a valid Python identifier.
207
208    Args:
209      test_method: The decorated test method.
210      testcases: (list of tuple/dict) A list of parameter
211                 tuples/dicts for individual test invocations.
212      naming_type: The test naming type, either _NAMED or _ARGUMENT_REPR.
213    """
214    self._test_method = test_method
215    self.testcases = testcases
216    self._naming_type = naming_type
217
218  def __call__(self, *args, **kwargs):
219    raise RuntimeError('You appear to be running a parameterized test case '
220                       'without having inherited from parameterized.'
221                       'TestCase. This is bad because none of '
222                       'your test cases are actually being run.')
223
224  def __iter__(self):
225    test_method = self._test_method
226    naming_type = self._naming_type
227
228    def MakeBoundParamTest(testcase_params):
229      @functools.wraps(test_method)
230      def BoundParamTest(self):
231        if isinstance(testcase_params, collections_abc.Mapping):
232          test_method(self, **testcase_params)
233        elif _NonStringIterable(testcase_params):
234          test_method(self, *testcase_params)
235        else:
236          test_method(self, testcase_params)
237
238      if naming_type is _FIRST_ARG:
239        # Signal the metaclass that the name of the test function is unique
240        # and descriptive.
241        BoundParamTest.__x_use_name__ = True
242        BoundParamTest.__name__ += str(testcase_params[0])
243        testcase_params = testcase_params[1:]
244      elif naming_type is _ARGUMENT_REPR:
245        # __x_extra_id__ is used to pass naming information to the __new__
246        # method of TestGeneratorMetaclass.
247        # The metaclass will make sure to create a unique, but nondescriptive
248        # name for this test.
249        BoundParamTest.__x_extra_id__ = '(%s)' % (
250            _FormatParameterList(testcase_params),)
251      else:
252        raise RuntimeError('%s is not a valid naming type.' % (naming_type,))
253
254      BoundParamTest.__doc__ = '%s(%s)' % (
255          BoundParamTest.__name__, _FormatParameterList(testcase_params))
256      if test_method.__doc__:
257        BoundParamTest.__doc__ += '\n%s' % (test_method.__doc__,)
258      return BoundParamTest
259    return (MakeBoundParamTest(c) for c in self.testcases)
260
261
262def _IsSingletonList(testcases):
263  """True iff testcases contains only a single non-tuple element."""
264  return len(testcases) == 1 and not isinstance(testcases[0], tuple)
265
266
267def _ModifyClass(class_object, testcases, naming_type):
268  assert not getattr(class_object, '_id_suffix', None), (
269      'Cannot add parameters to %s,'
270      ' which already has parameterized methods.' % (class_object,))
271  class_object._id_suffix = id_suffix = {}
272  # We change the size of __dict__ while we iterate over it,
273  # which Python 3.x will complain about, so use copy().
274  for name, obj in class_object.__dict__.copy().items():
275    if (name.startswith(unittest.TestLoader.testMethodPrefix)
276        and isinstance(obj, types.FunctionType)):
277      delattr(class_object, name)
278      methods = {}
279      _UpdateClassDictForParamTestCase(
280          methods, id_suffix, name,
281          _ParameterizedTestIter(obj, testcases, naming_type))
282      for name, meth in methods.items():
283        setattr(class_object, name, meth)
284
285
286def _ParameterDecorator(naming_type, testcases):
287  """Implementation of the parameterization decorators.
288
289  Args:
290    naming_type: The naming type.
291    testcases: Testcase parameters.
292
293  Returns:
294    A function for modifying the decorated object.
295  """
296  def _Apply(obj):
297    if isinstance(obj, type):
298      _ModifyClass(
299          obj,
300          list(testcases) if not isinstance(testcases, collections_abc.Sequence)
301          else testcases,
302          naming_type)
303      return obj
304    else:
305      return _ParameterizedTestIter(obj, testcases, naming_type)
306
307  if _IsSingletonList(testcases):
308    assert _NonStringIterable(testcases[0]), (
309        'Single parameter argument must be a non-string iterable')
310    testcases = testcases[0]
311
312  return _Apply
313
314
315def parameters(*testcases):  # pylint: disable=invalid-name
316  """A decorator for creating parameterized tests.
317
318  See the module docstring for a usage example.
319  Args:
320    *testcases: Parameters for the decorated method, either a single
321                iterable, or a list of tuples/dicts/objects (for tests
322                with only one argument).
323
324  Returns:
325     A test generator to be handled by TestGeneratorMetaclass.
326  """
327  return _ParameterDecorator(_ARGUMENT_REPR, testcases)
328
329
330def named_parameters(*testcases):  # pylint: disable=invalid-name
331  """A decorator for creating parameterized tests.
332
333  See the module docstring for a usage example. The first element of
334  each parameter tuple should be a string and will be appended to the
335  name of the test method.
336
337  Args:
338    *testcases: Parameters for the decorated method, either a single
339                iterable, or a list of tuples.
340
341  Returns:
342     A test generator to be handled by TestGeneratorMetaclass.
343  """
344  return _ParameterDecorator(_FIRST_ARG, testcases)
345
346
347class TestGeneratorMetaclass(type):
348  """Metaclass for test cases with test generators.
349
350  A test generator is an iterable in a testcase that produces callables. These
351  callables must be single-argument methods. These methods are injected into
352  the class namespace and the original iterable is removed. If the name of the
353  iterable conforms to the test pattern, the injected methods will be picked
354  up as tests by the unittest framework.
355
356  In general, it is supposed to be used in conjunction with the
357  parameters decorator.
358  """
359
360  def __new__(mcs, class_name, bases, dct):
361    dct['_id_suffix'] = id_suffix = {}
362    for name, obj in dct.items():
363      if (name.startswith(unittest.TestLoader.testMethodPrefix) and
364          _NonStringIterable(obj)):
365        iterator = iter(obj)
366        dct.pop(name)
367        _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator)
368
369    return type.__new__(mcs, class_name, bases, dct)
370
371
372def _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator):
373  """Adds individual test cases to a dictionary.
374
375  Args:
376    dct: The target dictionary.
377    id_suffix: The dictionary for mapping names to test IDs.
378    name: The original name of the test case.
379    iterator: The iterator generating the individual test cases.
380  """
381  for idx, func in enumerate(iterator):
382    assert callable(func), 'Test generators must yield callables, got %r' % (
383        func,)
384    if getattr(func, '__x_use_name__', False):
385      new_name = func.__name__
386    else:
387      new_name = '%s%s%d' % (name, _SEPARATOR, idx)
388    assert new_name not in dct, (
389        'Name of parameterized test case "%s" not unique' % (new_name,))
390    dct[new_name] = func
391    id_suffix[new_name] = getattr(func, '__x_extra_id__', '')
392
393
394class TestCase(unittest.TestCase):
395  """Base class for test cases using the parameters decorator."""
396  __metaclass__ = TestGeneratorMetaclass
397
398  def _OriginalName(self):
399    return self._testMethodName.split(_SEPARATOR)[0]
400
401  def __str__(self):
402    return '%s (%s)' % (self._OriginalName(), _StrClass(self.__class__))
403
404  def id(self):  # pylint: disable=invalid-name
405    """Returns the descriptive ID of the test.
406
407    This is used internally by the unittesting framework to get a name
408    for the test to be used in reports.
409
410    Returns:
411      The test id.
412    """
413    return '%s.%s%s' % (_StrClass(self.__class__),
414                        self._OriginalName(),
415                        self._id_suffix.get(self._testMethodName, ''))
416
417
418def CoopTestCase(other_base_class):
419  """Returns a new base class with a cooperative metaclass base.
420
421  This enables the TestCase to be used in combination
422  with other base classes that have custom metaclasses, such as
423  mox.MoxTestBase.
424
425  Only works with metaclasses that do not override type.__new__.
426
427  Example:
428
429    import google3
430    import mox
431
432    from google3.testing.pybase import parameterized
433
434    class ExampleTest(parameterized.CoopTestCase(mox.MoxTestBase)):
435      ...
436
437  Args:
438    other_base_class: (class) A test case base class.
439
440  Returns:
441    A new class object.
442  """
443  metaclass = type(
444      'CoopMetaclass',
445      (other_base_class.__metaclass__,
446       TestGeneratorMetaclass), {})
447  return metaclass(
448      'CoopTestCase',
449      (other_base_class, TestCase), {})
450