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.ParameterizedTestCase):
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.ParameterizedTestCase):
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 NamedParameters 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.ParameterizedTestCase):
86    @parameterized.NamedParameters(
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
106ParameterizedTestCase 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.ParameterizedTestCase):
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.ParameterizedTestCase):
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.ParameterizedTestCase):
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 collections
149import functools
150import re
151import types
152try:
153  import unittest2 as unittest
154except ImportError:
155  import unittest
156import uuid
157
158import six
159
160ADDR_RE = re.compile(r'\<([a-zA-Z0-9_\-\.]+) object at 0x[a-fA-F0-9]+\>')
161_SEPARATOR = uuid.uuid1().hex
162_FIRST_ARG = object()
163_ARGUMENT_REPR = object()
164
165
166def _CleanRepr(obj):
167  return ADDR_RE.sub(r'<\1>', repr(obj))
168
169
170# Helper function formerly from the unittest module, removed from it in
171# Python 2.7.
172def _StrClass(cls):
173  return '%s.%s' % (cls.__module__, cls.__name__)
174
175
176def _NonStringIterable(obj):
177  return (isinstance(obj, collections.Iterable) and not
178          isinstance(obj, six.string_types))
179
180
181def _FormatParameterList(testcase_params):
182  if isinstance(testcase_params, collections.Mapping):
183    return ', '.join('%s=%s' % (argname, _CleanRepr(value))
184                     for argname, value in testcase_params.items())
185  elif _NonStringIterable(testcase_params):
186    return ', '.join(map(_CleanRepr, testcase_params))
187  else:
188    return _FormatParameterList((testcase_params,))
189
190
191class _ParameterizedTestIter(object):
192  """Callable and iterable class for producing new test cases."""
193
194  def __init__(self, test_method, testcases, naming_type):
195    """Returns concrete test functions for a test and a list of parameters.
196
197    The naming_type is used to determine the name of the concrete
198    functions as reported by the unittest framework. If naming_type is
199    _FIRST_ARG, the testcases must be tuples, and the first element must
200    have a string representation that is a valid Python identifier.
201
202    Args:
203      test_method: The decorated test method.
204      testcases: (list of tuple/dict) A list of parameter
205                 tuples/dicts for individual test invocations.
206      naming_type: The test naming type, either _NAMED or _ARGUMENT_REPR.
207    """
208    self._test_method = test_method
209    self.testcases = testcases
210    self._naming_type = naming_type
211
212  def __call__(self, *args, **kwargs):
213    raise RuntimeError('You appear to be running a parameterized test case '
214                       'without having inherited from parameterized.'
215                       'ParameterizedTestCase. This is bad because none of '
216                       'your test cases are actually being run.')
217
218  def __iter__(self):
219    test_method = self._test_method
220    naming_type = self._naming_type
221
222    def MakeBoundParamTest(testcase_params):
223      @functools.wraps(test_method)
224      def BoundParamTest(self):
225        if isinstance(testcase_params, collections.Mapping):
226          test_method(self, **testcase_params)
227        elif _NonStringIterable(testcase_params):
228          test_method(self, *testcase_params)
229        else:
230          test_method(self, testcase_params)
231
232      if naming_type is _FIRST_ARG:
233        # Signal the metaclass that the name of the test function is unique
234        # and descriptive.
235        BoundParamTest.__x_use_name__ = True
236        BoundParamTest.__name__ += str(testcase_params[0])
237        testcase_params = testcase_params[1:]
238      elif naming_type is _ARGUMENT_REPR:
239        # __x_extra_id__ is used to pass naming information to the __new__
240        # method of TestGeneratorMetaclass.
241        # The metaclass will make sure to create a unique, but nondescriptive
242        # name for this test.
243        BoundParamTest.__x_extra_id__ = '(%s)' % (
244            _FormatParameterList(testcase_params),)
245      else:
246        raise RuntimeError('%s is not a valid naming type.' % (naming_type,))
247
248      BoundParamTest.__doc__ = '%s(%s)' % (
249          BoundParamTest.__name__, _FormatParameterList(testcase_params))
250      if test_method.__doc__:
251        BoundParamTest.__doc__ += '\n%s' % (test_method.__doc__,)
252      return BoundParamTest
253    return (MakeBoundParamTest(c) for c in self.testcases)
254
255
256def _IsSingletonList(testcases):
257  """True iff testcases contains only a single non-tuple element."""
258  return len(testcases) == 1 and not isinstance(testcases[0], tuple)
259
260
261def _ModifyClass(class_object, testcases, naming_type):
262  assert not getattr(class_object, '_id_suffix', None), (
263      'Cannot add parameters to %s,'
264      ' which already has parameterized methods.' % (class_object,))
265  class_object._id_suffix = id_suffix = {}
266  # We change the size of __dict__ while we iterate over it,
267  # which Python 3.x will complain about, so use copy().
268  for name, obj in class_object.__dict__.copy().items():
269    if (name.startswith(unittest.TestLoader.testMethodPrefix)
270        and isinstance(obj, types.FunctionType)):
271      delattr(class_object, name)
272      methods = {}
273      _UpdateClassDictForParamTestCase(
274          methods, id_suffix, name,
275          _ParameterizedTestIter(obj, testcases, naming_type))
276      for name, meth in methods.items():
277        setattr(class_object, name, meth)
278
279
280def _ParameterDecorator(naming_type, testcases):
281  """Implementation of the parameterization decorators.
282
283  Args:
284    naming_type: The naming type.
285    testcases: Testcase parameters.
286
287  Returns:
288    A function for modifying the decorated object.
289  """
290  def _Apply(obj):
291    if isinstance(obj, type):
292      _ModifyClass(
293          obj,
294          list(testcases) if not isinstance(testcases, collections.Sequence)
295          else testcases,
296          naming_type)
297      return obj
298    else:
299      return _ParameterizedTestIter(obj, testcases, naming_type)
300
301  if _IsSingletonList(testcases):
302    assert _NonStringIterable(testcases[0]), (
303        'Single parameter argument must be a non-string iterable')
304    testcases = testcases[0]
305
306  return _Apply
307
308
309def Parameters(*testcases):
310  """A decorator for creating parameterized tests.
311
312  See the module docstring for a usage example.
313  Args:
314    *testcases: Parameters for the decorated method, either a single
315                iterable, or a list of tuples/dicts/objects (for tests
316                with only one argument).
317
318  Returns:
319     A test generator to be handled by TestGeneratorMetaclass.
320  """
321  return _ParameterDecorator(_ARGUMENT_REPR, testcases)
322
323
324def NamedParameters(*testcases):
325  """A decorator for creating parameterized tests.
326
327  See the module docstring for a usage example. The first element of
328  each parameter tuple should be a string and will be appended to the
329  name of the test method.
330
331  Args:
332    *testcases: Parameters for the decorated method, either a single
333                iterable, or a list of tuples.
334
335  Returns:
336     A test generator to be handled by TestGeneratorMetaclass.
337  """
338  return _ParameterDecorator(_FIRST_ARG, testcases)
339
340
341class TestGeneratorMetaclass(type):
342  """Metaclass for test cases with test generators.
343
344  A test generator is an iterable in a testcase that produces callables. These
345  callables must be single-argument methods. These methods are injected into
346  the class namespace and the original iterable is removed. If the name of the
347  iterable conforms to the test pattern, the injected methods will be picked
348  up as tests by the unittest framework.
349
350  In general, it is supposed to be used in conjuction with the
351  Parameters decorator.
352  """
353
354  def __new__(mcs, class_name, bases, dct):
355    dct['_id_suffix'] = id_suffix = {}
356    for name, obj in dct.items():
357      if (name.startswith(unittest.TestLoader.testMethodPrefix) and
358          _NonStringIterable(obj)):
359        iterator = iter(obj)
360        dct.pop(name)
361        _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator)
362
363    return type.__new__(mcs, class_name, bases, dct)
364
365
366def _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator):
367  """Adds individual test cases to a dictionary.
368
369  Args:
370    dct: The target dictionary.
371    id_suffix: The dictionary for mapping names to test IDs.
372    name: The original name of the test case.
373    iterator: The iterator generating the individual test cases.
374  """
375  for idx, func in enumerate(iterator):
376    assert callable(func), 'Test generators must yield callables, got %r' % (
377        func,)
378    if getattr(func, '__x_use_name__', False):
379      new_name = func.__name__
380    else:
381      new_name = '%s%s%d' % (name, _SEPARATOR, idx)
382    assert new_name not in dct, (
383        'Name of parameterized test case "%s" not unique' % (new_name,))
384    dct[new_name] = func
385    id_suffix[new_name] = getattr(func, '__x_extra_id__', '')
386
387
388class ParameterizedTestCase(unittest.TestCase):
389  """Base class for test cases using the Parameters decorator."""
390  __metaclass__ = TestGeneratorMetaclass
391
392  def _OriginalName(self):
393    return self._testMethodName.split(_SEPARATOR)[0]
394
395  def __str__(self):
396    return '%s (%s)' % (self._OriginalName(), _StrClass(self.__class__))
397
398  def id(self):  # pylint: disable=invalid-name
399    """Returns the descriptive ID of the test.
400
401    This is used internally by the unittesting framework to get a name
402    for the test to be used in reports.
403
404    Returns:
405      The test id.
406    """
407    return '%s.%s%s' % (_StrClass(self.__class__),
408                        self._OriginalName(),
409                        self._id_suffix.get(self._testMethodName, ''))
410
411
412def CoopParameterizedTestCase(other_base_class):
413  """Returns a new base class with a cooperative metaclass base.
414
415  This enables the ParameterizedTestCase to be used in combination
416  with other base classes that have custom metaclasses, such as
417  mox.MoxTestBase.
418
419  Only works with metaclasses that do not override type.__new__.
420
421  Example:
422
423    import google3
424    import mox
425
426    from google3.testing.pybase import parameterized
427
428    class ExampleTest(parameterized.CoopParameterizedTestCase(mox.MoxTestBase)):
429      ...
430
431  Args:
432    other_base_class: (class) A test case base class.
433
434  Returns:
435    A new class object.
436  """
437  metaclass = type(
438      'CoopMetaclass',
439      (other_base_class.__metaclass__,
440       TestGeneratorMetaclass), {})
441  return metaclass(
442      'CoopParameterizedTestCase',
443      (other_base_class, ParameterizedTestCase), {})
444