1# Copyright 2017 The Abseil Authors.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Adds support for parameterized tests to Python's unittest TestCase class.
16
17A parameterized test is a method in a test case that is invoked with different
18argument tuples.
19
20A simple example::
21
22    class AdditionExample(parameterized.TestCase):
23      @parameterized.parameters(
24        (1, 2, 3),
25        (4, 5, 9),
26        (1, 1, 3))
27      def testAddition(self, op1, op2, result):
28        self.assertEqual(result, op1 + op2)
29
30Each invocation is a separate test case and properly isolated just
31like a normal test method, with its own setUp/tearDown cycle. In the
32example above, there are three separate testcases, one of which will
33fail due to an assertion error (1 + 1 != 3).
34
35Parameters for individual test cases can be tuples (with positional parameters)
36or dictionaries (with named parameters)::
37
38    class AdditionExample(parameterized.TestCase):
39      @parameterized.parameters(
40        {'op1': 1, 'op2': 2, 'result': 3},
41        {'op1': 4, 'op2': 5, 'result': 9},
42      )
43      def testAddition(self, op1, op2, result):
44        self.assertEqual(result, op1 + op2)
45
46If a parameterized test fails, the error message will show the
47original test name and the parameters for that test.
48
49The id method of the test, used internally by the unittest framework, is also
50modified to show the arguments (but note that the name reported by `id()`
51doesn't match the actual test name, see below). To make sure that test names
52stay the same across several invocations, object representations like::
53
54    >>> class Foo(object):
55    ...  pass
56    >>> repr(Foo())
57    '<__main__.Foo object at 0x23d8610>'
58
59are turned into ``__main__.Foo``. When selecting a subset of test cases to run
60on the command-line, the test cases contain an index suffix for each argument
61in the order they were passed to :func:`parameters` (eg. testAddition0,
62testAddition1, etc.) This naming scheme is subject to change; for more reliable
63and stable names, especially in test logs, use :func:`named_parameters` instead.
64
65Tests using :func:`named_parameters` are similar to :func:`parameters`, except
66only tuples or dicts of args are supported. For tuples, the first parameter arg
67has to be a string (or an object that returns an apt name when converted via
68``str()``). For dicts, a value for the key ``testcase_name`` must be present and
69must be a string (or an object that returns an apt name when converted via
70``str()``)::
71
72    class NamedExample(parameterized.TestCase):
73      @parameterized.named_parameters(
74        ('Normal', 'aa', 'aaa', True),
75        ('EmptyPrefix', '', 'abc', True),
76        ('BothEmpty', '', '', True))
77      def testStartsWith(self, prefix, string, result):
78        self.assertEqual(result, string.startswith(prefix))
79
80    class NamedExample(parameterized.TestCase):
81      @parameterized.named_parameters(
82        {'testcase_name': 'Normal',
83          'result': True, 'string': 'aaa', 'prefix': 'aa'},
84        {'testcase_name': 'EmptyPrefix',
85          'result': True, 'string': 'abc', 'prefix': ''},
86        {'testcase_name': 'BothEmpty',
87          'result': True, 'string': '', 'prefix': ''})
88      def testStartsWith(self, prefix, string, result):
89        self.assertEqual(result, string.startswith(prefix))
90
91Named tests also have the benefit that they can be run individually
92from the command line::
93
94    $ testmodule.py NamedExample.testStartsWithNormal
95    .
96    --------------------------------------------------------------------
97    Ran 1 test in 0.000s
98
99    OK
100
101Parameterized Classes
102=====================
103
104If invocation arguments are shared across test methods in a single
105TestCase class, instead of decorating all test methods
106individually, the class itself can be decorated::
107
108    @parameterized.parameters(
109      (1, 2, 3),
110      (4, 5, 9))
111    class ArithmeticTest(parameterized.TestCase):
112      def testAdd(self, arg1, arg2, result):
113        self.assertEqual(arg1 + arg2, result)
114
115      def testSubtract(self, arg1, arg2, result):
116        self.assertEqual(result - arg1, arg2)
117
118Inputs from Iterables
119=====================
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============================
135
136If a test method takes only one argument, the single arguments must not be
137wrapped into a tuple::
138
139    class NegativeNumberExample(parameterized.TestCase):
140      @parameterized.parameters(
141        -1, -3, -4, -5
142      )
143      def testIsNegative(self, arg):
144        self.assertTrue(IsNegative(arg))
145
146
147List/tuple as a Single Argument
148===============================
149
150If a test method takes a single argument of a list/tuple, it must be wrapped
151inside a tuple::
152
153    class ZeroSumExample(parameterized.TestCase):
154      @parameterized.parameters(
155        ([-1, 0, 1], ),
156        ([-2, 0, 2], ),
157      )
158      def testSumIsZero(self, arg):
159        self.assertEqual(0, sum(arg))
160
161
162Cartesian product of Parameter Values as Parameterized Test Cases
163=================================================================
164
165If required to test method over a cartesian product of parameters,
166`parameterized.product` may be used to facilitate generation of parameters
167test combinations::
168
169    class TestModuloExample(parameterized.TestCase):
170      @parameterized.product(
171          num=[0, 20, 80],
172          modulo=[2, 4],
173          expected=[0]
174      )
175      def testModuloResult(self, num, modulo, expected):
176        self.assertEqual(expected, num % modulo)
177
178This results in 6 test cases being created - one for each combination of the
179parameters. It is also possible to supply sequences of keyword argument dicts
180as elements of the cartesian product::
181
182    @parameterized.product(
183        (dict(num=5, modulo=3, expected=2),
184         dict(num=7, modulo=4, expected=3)),
185        dtype=(int, float)
186    )
187    def testModuloResult(self, num, modulo, expected, dtype):
188      self.assertEqual(expected, dtype(num) % modulo)
189
190This results in 4 test cases being created - for each of the two sets of test
191data (supplied as kwarg dicts) and for each of the two data types (supplied as
192a named parameter). Multiple keyword argument dicts may be supplied if required.
193
194Async Support
195=============
196
197If a test needs to call async functions, it can inherit from both
198parameterized.TestCase and another TestCase that supports async calls, such
199as [asynctest](https://github.com/Martiusweb/asynctest)::
200
201  import asynctest
202
203  class AsyncExample(parameterized.TestCase, asynctest.TestCase):
204    @parameterized.parameters(
205      ('a', 1),
206      ('b', 2),
207    )
208    async def testSomeAsyncFunction(self, arg, expected):
209      actual = await someAsyncFunction(arg)
210      self.assertEqual(actual, expected)
211"""
212
213from collections import abc
214import functools
215import inspect
216import itertools
217import re
218import types
219import unittest
220import warnings
221
222from absl.testing import absltest
223
224
225_ADDR_RE = re.compile(r'\<([a-zA-Z0-9_\-\.]+) object at 0x[a-fA-F0-9]+\>')
226_NAMED = object()
227_ARGUMENT_REPR = object()
228_NAMED_DICT_KEY = 'testcase_name'
229
230
231class NoTestsError(Exception):
232  """Raised when parameterized decorators do not generate any tests."""
233
234
235class DuplicateTestNameError(Exception):
236  """Raised when a parameterized test has the same test name multiple times."""
237
238  def __init__(self, test_class_name, new_test_name, original_test_name):
239    super(DuplicateTestNameError, self).__init__(
240        'Duplicate parameterized test name in {}: generated test name {!r} '
241        '(generated from {!r}) already exists. Consider using '
242        'named_parameters() to give your tests unique names and/or renaming '
243        'the conflicting test method.'.format(
244            test_class_name, new_test_name, original_test_name))
245
246
247def _clean_repr(obj):
248  return _ADDR_RE.sub(r'<\1>', repr(obj))
249
250
251def _non_string_or_bytes_iterable(obj):
252  return (isinstance(obj, abc.Iterable) and not isinstance(obj, str) and
253          not isinstance(obj, bytes))
254
255
256def _format_parameter_list(testcase_params):
257  if isinstance(testcase_params, abc.Mapping):
258    return ', '.join('%s=%s' % (argname, _clean_repr(value))
259                     for argname, value in testcase_params.items())
260  elif _non_string_or_bytes_iterable(testcase_params):
261    return ', '.join(map(_clean_repr, testcase_params))
262  else:
263    return _format_parameter_list((testcase_params,))
264
265
266def _async_wrapped(func):
267  @functools.wraps(func)
268  async def wrapper(*args, **kwargs):
269    return await func(*args, **kwargs)
270  return wrapper
271
272
273class _ParameterizedTestIter(object):
274  """Callable and iterable class for producing new test cases."""
275
276  def __init__(self, test_method, testcases, naming_type, original_name=None):
277    """Returns concrete test functions for a test and a list of parameters.
278
279    The naming_type is used to determine the name of the concrete
280    functions as reported by the unittest framework. If naming_type is
281    _FIRST_ARG, the testcases must be tuples, and the first element must
282    have a string representation that is a valid Python identifier.
283
284    Args:
285      test_method: The decorated test method.
286      testcases: (list of tuple/dict) A list of parameter tuples/dicts for
287          individual test invocations.
288      naming_type: The test naming type, either _NAMED or _ARGUMENT_REPR.
289      original_name: The original test method name. When decorated on a test
290          method, None is passed to __init__ and test_method.__name__ is used.
291          Note test_method.__name__ might be different than the original defined
292          test method because of the use of other decorators. A more accurate
293          value is set by TestGeneratorMetaclass.__new__ later.
294    """
295    self._test_method = test_method
296    self.testcases = testcases
297    self._naming_type = naming_type
298    if original_name is None:
299      original_name = test_method.__name__
300    self._original_name = original_name
301    self.__name__ = _ParameterizedTestIter.__name__
302
303  def __call__(self, *args, **kwargs):
304    raise RuntimeError('You appear to be running a parameterized test case '
305                       'without having inherited from parameterized.'
306                       'TestCase. This is bad because none of '
307                       'your test cases are actually being run. You may also '
308                       'be using another decorator before the parameterized '
309                       'one, in which case you should reverse the order.')
310
311  def __iter__(self):
312    test_method = self._test_method
313    naming_type = self._naming_type
314
315    def make_bound_param_test(testcase_params):
316      @functools.wraps(test_method)
317      def bound_param_test(self):
318        if isinstance(testcase_params, abc.Mapping):
319          return test_method(self, **testcase_params)
320        elif _non_string_or_bytes_iterable(testcase_params):
321          return test_method(self, *testcase_params)
322        else:
323          return test_method(self, testcase_params)
324
325      if naming_type is _NAMED:
326        # Signal the metaclass that the name of the test function is unique
327        # and descriptive.
328        bound_param_test.__x_use_name__ = True
329
330        testcase_name = None
331        if isinstance(testcase_params, abc.Mapping):
332          if _NAMED_DICT_KEY not in testcase_params:
333            raise RuntimeError(
334                'Dict for named tests must contain key "%s"' % _NAMED_DICT_KEY)
335          # Create a new dict to avoid modifying the supplied testcase_params.
336          testcase_name = testcase_params[_NAMED_DICT_KEY]
337          testcase_params = {
338              k: v for k, v in testcase_params.items() if k != _NAMED_DICT_KEY
339          }
340        elif _non_string_or_bytes_iterable(testcase_params):
341          if not isinstance(testcase_params[0], str):
342            raise RuntimeError(
343                'The first element of named test parameters is the test name '
344                'suffix and must be a string')
345          testcase_name = testcase_params[0]
346          testcase_params = testcase_params[1:]
347        else:
348          raise RuntimeError(
349              'Named tests must be passed a dict or non-string iterable.')
350
351        test_method_name = self._original_name
352        # Support PEP-8 underscore style for test naming if used.
353        if (test_method_name.startswith('test_')
354            and testcase_name
355            and not testcase_name.startswith('_')):
356          test_method_name += '_'
357
358        bound_param_test.__name__ = test_method_name + str(testcase_name)
359      elif naming_type is _ARGUMENT_REPR:
360        # If it's a generator, convert it to a tuple and treat them as
361        # parameters.
362        if isinstance(testcase_params, types.GeneratorType):
363          testcase_params = tuple(testcase_params)
364        # The metaclass creates a unique, but non-descriptive method name for
365        # _ARGUMENT_REPR tests using an indexed suffix.
366        # To keep test names descriptive, only the original method name is used.
367        # To make sure test names are unique, we add a unique descriptive suffix
368        # __x_params_repr__ for every test.
369        params_repr = '(%s)' % (_format_parameter_list(testcase_params),)
370        bound_param_test.__x_params_repr__ = params_repr
371      else:
372        raise RuntimeError('%s is not a valid naming type.' % (naming_type,))
373
374      bound_param_test.__doc__ = '%s(%s)' % (
375          bound_param_test.__name__, _format_parameter_list(testcase_params))
376      if test_method.__doc__:
377        bound_param_test.__doc__ += '\n%s' % (test_method.__doc__,)
378      if inspect.iscoroutinefunction(test_method):
379        return _async_wrapped(bound_param_test)
380      return bound_param_test
381
382    return (make_bound_param_test(c) for c in self.testcases)
383
384
385def _modify_class(class_object, testcases, naming_type):
386  assert not getattr(class_object, '_test_params_reprs', None), (
387      'Cannot add parameters to %s. Either it already has parameterized '
388      'methods, or its super class is also a parameterized class.' % (
389          class_object,))
390  # NOTE: _test_params_repr is private to parameterized.TestCase and it's
391  # metaclass; do not use it outside of those classes.
392  class_object._test_params_reprs = test_params_reprs = {}
393  for name, obj in class_object.__dict__.copy().items():
394    if (name.startswith(unittest.TestLoader.testMethodPrefix)
395        and isinstance(obj, types.FunctionType)):
396      delattr(class_object, name)
397      methods = {}
398      _update_class_dict_for_param_test_case(
399          class_object.__name__, methods, test_params_reprs, name,
400          _ParameterizedTestIter(obj, testcases, naming_type, name))
401      for meth_name, meth in methods.items():
402        setattr(class_object, meth_name, meth)
403
404
405def _parameter_decorator(naming_type, testcases):
406  """Implementation of the parameterization decorators.
407
408  Args:
409    naming_type: The naming type.
410    testcases: Testcase parameters.
411
412  Raises:
413    NoTestsError: Raised when the decorator generates no tests.
414
415  Returns:
416    A function for modifying the decorated object.
417  """
418  def _apply(obj):
419    if isinstance(obj, type):
420      _modify_class(obj, testcases, naming_type)
421      return obj
422    else:
423      return _ParameterizedTestIter(obj, testcases, naming_type)
424
425  if (len(testcases) == 1 and
426      not isinstance(testcases[0], tuple) and
427      not isinstance(testcases[0], abc.Mapping)):
428    # Support using a single non-tuple parameter as a list of test cases.
429    # Note that the single non-tuple parameter can't be Mapping either, which
430    # means a single dict parameter case.
431    assert _non_string_or_bytes_iterable(testcases[0]), (
432        'Single parameter argument must be a non-string non-Mapping iterable')
433    testcases = testcases[0]
434
435  if not isinstance(testcases, abc.Sequence):
436    testcases = list(testcases)
437  if not testcases:
438    raise NoTestsError(
439        'parameterized test decorators did not generate any tests. '
440        'Make sure you specify non-empty parameters, '
441        'and do not reuse generators more than once.')
442
443  return _apply
444
445
446def parameters(*testcases):
447  """A decorator for creating parameterized tests.
448
449  See the module docstring for a usage example.
450
451  Args:
452    *testcases: Parameters for the decorated method, either a single
453        iterable, or a list of tuples/dicts/objects (for tests with only one
454        argument).
455
456  Raises:
457    NoTestsError: Raised when the decorator generates no tests.
458
459  Returns:
460     A test generator to be handled by TestGeneratorMetaclass.
461  """
462  return _parameter_decorator(_ARGUMENT_REPR, testcases)
463
464
465def named_parameters(*testcases):
466  """A decorator for creating parameterized tests.
467
468  See the module docstring for a usage example. For every parameter tuple
469  passed, the first element of the tuple should be a string and will be appended
470  to the name of the test method. Each parameter dict passed must have a value
471  for the key "testcase_name", the string representation of that value will be
472  appended to the name of the test method.
473
474  Args:
475    *testcases: Parameters for the decorated method, either a single iterable,
476        or a list of tuples or dicts.
477
478  Raises:
479    NoTestsError: Raised when the decorator generates no tests.
480
481  Returns:
482     A test generator to be handled by TestGeneratorMetaclass.
483  """
484  return _parameter_decorator(_NAMED, testcases)
485
486
487def product(*kwargs_seqs, **testgrid):
488  """A decorator for running tests over cartesian product of parameters values.
489
490  See the module docstring for a usage example. The test will be run for every
491  possible combination of the parameters.
492
493  Args:
494    *kwargs_seqs: Each positional parameter is a sequence of keyword arg dicts;
495      every test case generated will include exactly one kwargs dict from each
496      positional parameter; these will then be merged to form an overall list
497      of arguments for the test case.
498    **testgrid: A mapping of parameter names and their possible values. Possible
499      values should given as either a list or a tuple.
500
501  Raises:
502    NoTestsError: Raised when the decorator generates no tests.
503
504  Returns:
505     A test generator to be handled by TestGeneratorMetaclass.
506  """
507
508  for name, values in testgrid.items():
509    assert isinstance(values, (list, tuple)), (
510        'Values of {} must be given as list or tuple, found {}'.format(
511            name, type(values)))
512
513  prior_arg_names = set()
514  for kwargs_seq in kwargs_seqs:
515    assert ((isinstance(kwargs_seq, (list, tuple))) and
516            all(isinstance(kwargs, dict) for kwargs in kwargs_seq)), (
517                'Positional parameters must be a sequence of keyword arg'
518                'dicts, found {}'
519                .format(kwargs_seq))
520    if kwargs_seq:
521      arg_names = set(kwargs_seq[0])
522      assert all(set(kwargs) == arg_names for kwargs in kwargs_seq), (
523          'Keyword argument dicts within a single parameter must all have the '
524          'same keys, found {}'.format(kwargs_seq))
525      assert not (arg_names & prior_arg_names), (
526          'Keyword argument dict sequences must all have distinct argument '
527          'names, found duplicate(s) {}'
528          .format(sorted(arg_names & prior_arg_names)))
529      prior_arg_names |= arg_names
530
531  assert not (prior_arg_names & set(testgrid)), (
532      'Arguments supplied in kwargs dicts in positional parameters must not '
533      'overlap with arguments supplied as named parameters; found duplicate '
534      'argument(s) {}'.format(sorted(prior_arg_names & set(testgrid))))
535
536  # Convert testgrid into a sequence of sequences of kwargs dicts and combine
537  # with the positional parameters.
538  # So foo=[1,2], bar=[3,4] --> [[{foo: 1}, {foo: 2}], [{bar: 3, bar: 4}]]
539  testgrid = (tuple({k: v} for v in vs) for k, vs in testgrid.items())
540  testgrid = tuple(kwargs_seqs) + tuple(testgrid)
541
542  # Create all possible combinations of parameters as a cartesian product
543  # of parameter values.
544  testcases = [
545      dict(itertools.chain.from_iterable(case.items()
546                                         for case in cases))
547      for cases in itertools.product(*testgrid)
548  ]
549  return _parameter_decorator(_ARGUMENT_REPR, testcases)
550
551
552class TestGeneratorMetaclass(type):
553  """Metaclass for adding tests generated by parameterized decorators."""
554
555  def __new__(cls, class_name, bases, dct):
556    # NOTE: _test_params_repr is private to parameterized.TestCase and it's
557    # metaclass; do not use it outside of those classes.
558    test_params_reprs = dct.setdefault('_test_params_reprs', {})
559    for name, obj in dct.copy().items():
560      if (name.startswith(unittest.TestLoader.testMethodPrefix) and
561          _non_string_or_bytes_iterable(obj)):
562        # NOTE: `obj` might not be a _ParameterizedTestIter in two cases:
563        # 1. a class-level iterable named test* that isn't a test, such as
564        #    a list of something. Such attributes get deleted from the class.
565        #
566        # 2. If a decorator is applied to the parameterized test, e.g.
567        #    @morestuff
568        #    @parameterized.parameters(...)
569        #    def test_foo(...): ...
570        #
571        #   This is OK so long as the underlying parameterized function state
572        #   is forwarded (e.g. using functool.wraps() and **without**
573        #   accessing explicitly accessing the internal attributes.
574        if isinstance(obj, _ParameterizedTestIter):
575          # Update the original test method name so it's more accurate.
576          # The mismatch might happen when another decorator is used inside
577          # the parameterized decrators, and the inner decorator doesn't
578          # preserve its __name__.
579          obj._original_name = name
580        iterator = iter(obj)
581        dct.pop(name)
582        _update_class_dict_for_param_test_case(
583            class_name, dct, test_params_reprs, name, iterator)
584    # If the base class is a subclass of parameterized.TestCase, inherit its
585    # _test_params_reprs too.
586    for base in bases:
587      # Check if the base has _test_params_reprs first, then check if it's a
588      # subclass of parameterized.TestCase. Otherwise when this is called for
589      # the parameterized.TestCase definition itself, this raises because
590      # itself is not defined yet. This works as long as absltest.TestCase does
591      # not define _test_params_reprs.
592      base_test_params_reprs = getattr(base, '_test_params_reprs', None)
593      if base_test_params_reprs and issubclass(base, TestCase):
594        for test_method, test_method_id in base_test_params_reprs.items():
595          # test_method may both exists in base and this class.
596          # This class's method overrides base class's.
597          # That's why it should only inherit it if it does not exist.
598          test_params_reprs.setdefault(test_method, test_method_id)
599
600    return type.__new__(cls, class_name, bases, dct)
601
602
603def _update_class_dict_for_param_test_case(
604    test_class_name, dct, test_params_reprs, name, iterator):
605  """Adds individual test cases to a dictionary.
606
607  Args:
608    test_class_name: The name of the class tests are added to.
609    dct: The target dictionary.
610    test_params_reprs: The dictionary for mapping names to test IDs.
611    name: The original name of the test case.
612    iterator: The iterator generating the individual test cases.
613
614  Raises:
615    DuplicateTestNameError: Raised when a test name occurs multiple times.
616    RuntimeError: If non-parameterized functions are generated.
617  """
618  for idx, func in enumerate(iterator):
619    assert callable(func), 'Test generators must yield callables, got %r' % (
620        func,)
621    if not (getattr(func, '__x_use_name__', None) or
622            getattr(func, '__x_params_repr__', None)):
623      raise RuntimeError(
624          '{}.{} generated a test function without using the parameterized '
625          'decorators. Only tests generated using the decorators are '
626          'supported.'.format(test_class_name, name))
627
628    if getattr(func, '__x_use_name__', False):
629      original_name = func.__name__
630      new_name = original_name
631    else:
632      original_name = name
633      new_name = '%s%d' % (original_name, idx)
634
635    if new_name in dct:
636      raise DuplicateTestNameError(test_class_name, new_name, original_name)
637
638    dct[new_name] = func
639    test_params_reprs[new_name] = getattr(func, '__x_params_repr__', '')
640
641
642class TestCase(absltest.TestCase, metaclass=TestGeneratorMetaclass):
643  """Base class for test cases using the parameters decorator."""
644
645  # visibility: private; do not call outside this class.
646  def _get_params_repr(self):
647    return self._test_params_reprs.get(self._testMethodName, '')
648
649  def __str__(self):
650    params_repr = self._get_params_repr()
651    if params_repr:
652      params_repr = ' ' + params_repr
653    return '{}{} ({})'.format(
654        self._testMethodName, params_repr,
655        unittest.util.strclass(self.__class__))
656
657  def id(self):
658    """Returns the descriptive ID of the test.
659
660    This is used internally by the unittesting framework to get a name
661    for the test to be used in reports.
662
663    Returns:
664      The test id.
665    """
666    base = super(TestCase, self).id()
667    params_repr = self._get_params_repr()
668    if params_repr:
669      # We include the params in the id so that, when reported in the
670      # test.xml file, the value is more informative than just "test_foo0".
671      # Use a space to separate them so that it's copy/paste friendly and
672      # easy to identify the actual test id.
673      return '{} {}'.format(base, params_repr)
674    else:
675      return base
676
677
678# This function is kept CamelCase because it's used as a class's base class.
679def CoopTestCase(other_base_class):  # pylint: disable=invalid-name
680  """Returns a new base class with a cooperative metaclass base.
681
682  This enables the TestCase to be used in combination
683  with other base classes that have custom metaclasses, such as
684  ``mox.MoxTestBase``.
685
686  Only works with metaclasses that do not override ``type.__new__``.
687
688  Example::
689
690      from absl.testing import parameterized
691
692      class ExampleTest(parameterized.CoopTestCase(OtherTestCase)):
693        ...
694
695  Args:
696    other_base_class: (class) A test case base class.
697
698  Returns:
699    A new class object.
700  """
701  # If the other base class has a metaclass of 'type' then trying to combine
702  # the metaclasses will result in an MRO error. So simply combine them and
703  # return.
704  if type(other_base_class) == type:  # pylint: disable=unidiomatic-typecheck
705    warnings.warn(
706        'CoopTestCase is only necessary when combining with a class that uses'
707        ' a metaclass. Use multiple inheritance like this instead: class'
708        f' ExampleTest(paramaterized.TestCase, {other_base_class.__name__}):',
709        stacklevel=2,
710    )
711
712    class CoopTestCaseBase(other_base_class, TestCase):
713      pass
714
715    return CoopTestCaseBase
716  else:
717
718    class CoopMetaclass(type(other_base_class), TestGeneratorMetaclass):  # pylint: disable=unused-variable
719      pass
720
721    class CoopTestCaseBase(other_base_class, TestCase, metaclass=CoopMetaclass):
722      pass
723
724    return CoopTestCaseBase
725