Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
_parameterized.py421 linesDownload Raw Back to internal
1#! /usr/bin/env python2#3# Protocol Buffers - Google's data interchange format4# Copyright 2008 Google Inc.  All rights reserved.5#6# Use of this source code is governed by a BSD-style7# license that can be found in the LICENSE file or at8# https://developers.google.com/open-source/licenses/bsd9 10"""Adds support for parameterized tests to Python's unittest TestCase class.11 12A parameterized test is a method in a test case that is invoked with different13argument tuples.14 15A simple example:16 17  class AdditionExample(_parameterized.TestCase):18    @_parameterized.parameters(19       (1, 2, 3),20       (4, 5, 9),21       (1, 1, 3))22    def testAddition(self, op1, op2, result):23      self.assertEqual(result, op1 + op2)24 25 26Each invocation is a separate test case and properly isolated just27like a normal test method, with its own setUp/tearDown cycle. In the28example above, there are three separate testcases, one of which will29fail due to an assertion error (1 + 1 != 3).30 31Parameters for individual test cases can be tuples (with positional parameters)32or dictionaries (with named parameters):33 34  class AdditionExample(_parameterized.TestCase):35    @_parameterized.parameters(36       {'op1': 1, 'op2': 2, 'result': 3},37       {'op1': 4, 'op2': 5, 'result': 9},38    )39    def testAddition(self, op1, op2, result):40      self.assertEqual(result, op1 + op2)41 42If a parameterized test fails, the error message will show the43original test name (which is modified internally) and the arguments44for the specific invocation, which are part of the string returned by45the shortDescription() method on test cases.46 47The id method of the test, used internally by the unittest framework,48is also modified to show the arguments. To make sure that test names49stay the same across several invocations, object representations like50 51  >>> class Foo(object):52  ...  pass53  >>> repr(Foo())54  '<__main__.Foo object at 0x23d8610>'55 56are turned into '<__main__.Foo>'. For even more descriptive names,57especially in test logs, you can use the named_parameters decorator. In58this case, only tuples are supported, and the first parameters has to59be a string (or an object that returns an apt name when converted via60str()):61 62  class NamedExample(_parameterized.TestCase):63    @_parameterized.named_parameters(64       ('Normal', 'aa', 'aaa', True),65       ('EmptyPrefix', '', 'abc', True),66       ('BothEmpty', '', '', True))67    def testStartsWith(self, prefix, string, result):68      self.assertEqual(result, strings.startswith(prefix))69 70Named tests also have the benefit that they can be run individually71from the command line:72 73  $ testmodule.py NamedExample.testStartsWithNormal74  .75  --------------------------------------------------------------------76  Ran 1 test in 0.000s77 78  OK79 80Parameterized Classes81=====================82If invocation arguments are shared across test methods in a single83TestCase class, instead of decorating all test methods84individually, the class itself can be decorated:85 86  @_parameterized.parameters(87    (1, 2, 3)88    (4, 5, 9))89  class ArithmeticTest(_parameterized.TestCase):90    def testAdd(self, arg1, arg2, result):91      self.assertEqual(arg1 + arg2, result)92 93    def testSubtract(self, arg2, arg2, result):94      self.assertEqual(result - arg1, arg2)95 96Inputs from Iterables97=====================98If parameters should be shared across several test cases, or are dynamically99created from other sources, a single non-tuple iterable can be passed into100the decorator. This iterable will be used to obtain the test cases:101 102  class AdditionExample(_parameterized.TestCase):103    @_parameterized.parameters(104      c.op1, c.op2, c.result for c in testcases105    )106    def testAddition(self, op1, op2, result):107      self.assertEqual(result, op1 + op2)108 109 110Single-Argument Test Methods111============================112If a test method takes only one argument, the single argument does not need to113be wrapped into a tuple:114 115  class NegativeNumberExample(_parameterized.TestCase):116    @_parameterized.parameters(117       -1, -3, -4, -5118    )119    def testIsNegative(self, arg):120      self.assertTrue(IsNegative(arg))121"""122 123__author__ = 'tmarek@google.com (Torsten Marek)'124 125import functools126import re127import types128import unittest129import uuid130 131try:132  # Since python 3133  import collections.abc as collections_abc134except ImportError:135  # Won't work after python 3.8136  import collections as collections_abc137 138ADDR_RE = re.compile(r'\<([a-zA-Z0-9_\-\.]+) object at 0x[a-fA-F0-9]+\>')139_SEPARATOR = uuid.uuid1().hex140_FIRST_ARG = object()141_ARGUMENT_REPR = object()142 143 144def _CleanRepr(obj):145  return ADDR_RE.sub(r'<\1>', repr(obj))146 147 148# Helper function formerly from the unittest module, removed from it in149# Python 2.7.150def _StrClass(cls):151  return '%s.%s' % (cls.__module__, cls.__name__)152 153 154def _NonStringIterable(obj):155  return (isinstance(obj, collections_abc.Iterable) and156          not isinstance(obj, str))157 158 159def _FormatParameterList(testcase_params):160  if isinstance(testcase_params, collections_abc.Mapping):161    return ', '.join('%s=%s' % (argname, _CleanRepr(value))162                     for argname, value in testcase_params.items())163  elif _NonStringIterable(testcase_params):164    return ', '.join(map(_CleanRepr, testcase_params))165  else:166    return _FormatParameterList((testcase_params,))167 168 169class _ParameterizedTestIter(object):170  """Callable and iterable class for producing new test cases."""171 172  def __init__(self, test_method, testcases, naming_type):173    """Returns concrete test functions for a test and a list of parameters.174 175    The naming_type is used to determine the name of the concrete176    functions as reported by the unittest framework. If naming_type is177    _FIRST_ARG, the testcases must be tuples, and the first element must178    have a string representation that is a valid Python identifier.179 180    Args:181      test_method: The decorated test method.182      testcases: (list of tuple/dict) A list of parameter183                 tuples/dicts for individual test invocations.184      naming_type: The test naming type, either _NAMED or _ARGUMENT_REPR.185    """186    self._test_method = test_method187    self.testcases = testcases188    self._naming_type = naming_type189 190  def __call__(self, *args, **kwargs):191    raise RuntimeError('You appear to be running a parameterized test case '192                       'without having inherited from parameterized.'193                       'TestCase. This is bad because none of '194                       'your test cases are actually being run.')195 196  def __iter__(self):197    test_method = self._test_method198    naming_type = self._naming_type199 200    def MakeBoundParamTest(testcase_params):201      @functools.wraps(test_method)202      def BoundParamTest(self):203        if isinstance(testcase_params, collections_abc.Mapping):204          test_method(self, **testcase_params)205        elif _NonStringIterable(testcase_params):206          test_method(self, *testcase_params)207        else:208          test_method(self, testcase_params)209 210      if naming_type is _FIRST_ARG:211        # Signal the metaclass that the name of the test function is unique212        # and descriptive.213        BoundParamTest.__x_use_name__ = True214        BoundParamTest.__name__ += str(testcase_params[0])215        testcase_params = testcase_params[1:]216      elif naming_type is _ARGUMENT_REPR:217        # __x_extra_id__ is used to pass naming information to the __new__218        # method of TestGeneratorMetaclass.219        # The metaclass will make sure to create a unique, but nondescriptive220        # name for this test.221        BoundParamTest.__x_extra_id__ = '(%s)' % (222            _FormatParameterList(testcase_params),)223      else:224        raise RuntimeError('%s is not a valid naming type.' % (naming_type,))225 226      BoundParamTest.__doc__ = '%s(%s)' % (227          BoundParamTest.__name__, _FormatParameterList(testcase_params))228      if test_method.__doc__:229        BoundParamTest.__doc__ += '\n%s' % (test_method.__doc__,)230      return BoundParamTest231    return (MakeBoundParamTest(c) for c in self.testcases)232 233 234def _IsSingletonList(testcases):235  """True iff testcases contains only a single non-tuple element."""236  return len(testcases) == 1 and not isinstance(testcases[0], tuple)237 238 239def _ModifyClass(class_object, testcases, naming_type):240  assert not getattr(class_object, '_id_suffix', None), (241      'Cannot add parameters to %s,'242      ' which already has parameterized methods.' % (class_object,))243  class_object._id_suffix = id_suffix = {}244  # We change the size of __dict__ while we iterate over it,245  # which Python 3.x will complain about, so use copy().246  for name, obj in class_object.__dict__.copy().items():247    if (name.startswith(unittest.TestLoader.testMethodPrefix)248        and isinstance(obj, types.FunctionType)):249      delattr(class_object, name)250      methods = {}251      _UpdateClassDictForParamTestCase(252          methods, id_suffix, name,253          _ParameterizedTestIter(obj, testcases, naming_type))254      for name, meth in methods.items():255        setattr(class_object, name, meth)256 257 258def _ParameterDecorator(naming_type, testcases):259  """Implementation of the parameterization decorators.260 261  Args:262    naming_type: The naming type.263    testcases: Testcase parameters.264 265  Returns:266    A function for modifying the decorated object.267  """268  def _Apply(obj):269    if isinstance(obj, type):270      _ModifyClass(271          obj,272          list(testcases) if not isinstance(testcases, collections_abc.Sequence)273          else testcases,274          naming_type)275      return obj276    else:277      return _ParameterizedTestIter(obj, testcases, naming_type)278 279  if _IsSingletonList(testcases):280    assert _NonStringIterable(testcases[0]), (281        'Single parameter argument must be a non-string iterable')282    testcases = testcases[0]283 284  return _Apply285 286 287def parameters(*testcases):  # pylint: disable=invalid-name288  """A decorator for creating parameterized tests.289 290  See the module docstring for a usage example.291  Args:292    *testcases: Parameters for the decorated method, either a single293                iterable, or a list of tuples/dicts/objects (for tests294                with only one argument).295 296  Returns:297     A test generator to be handled by TestGeneratorMetaclass.298  """299  return _ParameterDecorator(_ARGUMENT_REPR, testcases)300 301 302def named_parameters(*testcases):  # pylint: disable=invalid-name303  """A decorator for creating parameterized tests.304 305  See the module docstring for a usage example. The first element of306  each parameter tuple should be a string and will be appended to the307  name of the test method.308 309  Args:310    *testcases: Parameters for the decorated method, either a single311                iterable, or a list of tuples.312 313  Returns:314     A test generator to be handled by TestGeneratorMetaclass.315  """316  return _ParameterDecorator(_FIRST_ARG, testcases)317 318 319class TestGeneratorMetaclass(type):320  """Metaclass for test cases with test generators.321 322  A test generator is an iterable in a testcase that produces callables. These323  callables must be single-argument methods. These methods are injected into324  the class namespace and the original iterable is removed. If the name of the325  iterable conforms to the test pattern, the injected methods will be picked326  up as tests by the unittest framework.327 328  In general, it is supposed to be used in conjunction with the329  parameters decorator.330  """331 332  def __new__(mcs, class_name, bases, dct):333    dct['_id_suffix'] = id_suffix = {}334    for name, obj in dct.copy().items():335      if (name.startswith(unittest.TestLoader.testMethodPrefix) and336          _NonStringIterable(obj)):337        iterator = iter(obj)338        dct.pop(name)339        _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator)340 341    return type.__new__(mcs, class_name, bases, dct)342 343 344def _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator):345  """Adds individual test cases to a dictionary.346 347  Args:348    dct: The target dictionary.349    id_suffix: The dictionary for mapping names to test IDs.350    name: The original name of the test case.351    iterator: The iterator generating the individual test cases.352  """353  for idx, func in enumerate(iterator):354    assert callable(func), 'Test generators must yield callables, got %r' % (355        func,)356    if getattr(func, '__x_use_name__', False):357      new_name = func.__name__358    else:359      new_name = '%s%s%d' % (name, _SEPARATOR, idx)360    assert new_name not in dct, (361        'Name of parameterized test case "%s" not unique' % (new_name,))362    dct[new_name] = func363    id_suffix[new_name] = getattr(func, '__x_extra_id__', '')364 365 366class TestCase(unittest.TestCase, metaclass=TestGeneratorMetaclass):367  """Base class for test cases using the parameters decorator."""368 369  def _OriginalName(self):370    return self._testMethodName.split(_SEPARATOR)[0]371 372  def __str__(self):373    return '%s (%s)' % (self._OriginalName(), _StrClass(self.__class__))374 375  def id(self):  # pylint: disable=invalid-name376    """Returns the descriptive ID of the test.377 378    This is used internally by the unittesting framework to get a name379    for the test to be used in reports.380 381    Returns:382      The test id.383    """384    return '%s.%s%s' % (_StrClass(self.__class__),385                        self._OriginalName(),386                        self._id_suffix.get(self._testMethodName, ''))387 388 389def CoopTestCase(other_base_class):390  """Returns a new base class with a cooperative metaclass base.391 392  This enables the TestCase to be used in combination393  with other base classes that have custom metaclasses, such as394  mox.MoxTestBase.395 396  Only works with metaclasses that do not override type.__new__.397 398  Example:399 400    import google3401    import mox402 403    from google.protobuf.internal import _parameterized404 405    class ExampleTest(parameterized.CoopTestCase(mox.MoxTestBase)):406      ...407 408  Args:409    other_base_class: (class) A test case base class.410 411  Returns:412    A new class object.413  """414  metaclass = type(415      'CoopMetaclass',416      (other_base_class.__metaclass__,417       TestGeneratorMetaclass), {})418  return metaclass(419      'CoopTestCase',420      (other_base_class, TestCase), {})421 
codekingpro/portable-devtools · Team Ai