* Rosy hack doesn't apply (that test should be removed for the open-source release). * Added our own copy of parameterized.py (the open-source version of Google Apputils doesn't contain it). * The C++ Descriptor object didn't implement extension_ranges. * Had to implement a hack around returning EncodeError, to work around the module-loading behavior of the test runner.pull/221/head
parent
ada6556785
commit
0b70a43736
11 changed files with 490 additions and 27 deletions
@ -0,0 +1,438 @@ |
||||
#! /usr/bin/python |
||||
# |
||||
# Protocol Buffers - Google's data interchange format |
||||
# Copyright 2008 Google Inc. All rights reserved. |
||||
# https://developers.google.com/protocol-buffers/ |
||||
# |
||||
# Redistribution and use in source and binary forms, with or without |
||||
# modification, are permitted provided that the following conditions are |
||||
# met: |
||||
# |
||||
# * Redistributions of source code must retain the above copyright |
||||
# notice, this list of conditions and the following disclaimer. |
||||
# * Redistributions in binary form must reproduce the above |
||||
# copyright notice, this list of conditions and the following disclaimer |
||||
# in the documentation and/or other materials provided with the |
||||
# distribution. |
||||
# * Neither the name of Google Inc. nor the names of its |
||||
# contributors may be used to endorse or promote products derived from |
||||
# this software without specific prior written permission. |
||||
# |
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
|
||||
"""Adds support for parameterized tests to Python's unittest TestCase class. |
||||
|
||||
A parameterized test is a method in a test case that is invoked with different |
||||
argument tuples. |
||||
|
||||
A simple example: |
||||
|
||||
class AdditionExample(parameterized.ParameterizedTestCase): |
||||
@parameterized.Parameters( |
||||
(1, 2, 3), |
||||
(4, 5, 9), |
||||
(1, 1, 3)) |
||||
def testAddition(self, op1, op2, result): |
||||
self.assertEquals(result, op1 + op2) |
||||
|
||||
|
||||
Each invocation is a separate test case and properly isolated just |
||||
like a normal test method, with its own setUp/tearDown cycle. In the |
||||
example above, there are three separate testcases, one of which will |
||||
fail due to an assertion error (1 + 1 != 3). |
||||
|
||||
Parameters for invididual test cases can be tuples (with positional parameters) |
||||
or dictionaries (with named parameters): |
||||
|
||||
class AdditionExample(parameterized.ParameterizedTestCase): |
||||
@parameterized.Parameters( |
||||
{'op1': 1, 'op2': 2, 'result': 3}, |
||||
{'op1': 4, 'op2': 5, 'result': 9}, |
||||
) |
||||
def testAddition(self, op1, op2, result): |
||||
self.assertEquals(result, op1 + op2) |
||||
|
||||
If a parameterized test fails, the error message will show the |
||||
original test name (which is modified internally) and the arguments |
||||
for the specific invocation, which are part of the string returned by |
||||
the shortDescription() method on test cases. |
||||
|
||||
The id method of the test, used internally by the unittest framework, |
||||
is also modified to show the arguments. To make sure that test names |
||||
stay the same across several invocations, object representations like |
||||
|
||||
>>> class Foo(object): |
||||
... pass |
||||
>>> repr(Foo()) |
||||
'<__main__.Foo object at 0x23d8610>' |
||||
|
||||
are turned into '<__main__.Foo>'. For even more descriptive names, |
||||
especially in test logs, you can use the NamedParameters decorator. In |
||||
this case, only tuples are supported, and the first parameters has to |
||||
be a string (or an object that returns an apt name when converted via |
||||
str()): |
||||
|
||||
class NamedExample(parameterized.ParameterizedTestCase): |
||||
@parameterized.NamedParameters( |
||||
('Normal', 'aa', 'aaa', True), |
||||
('EmptyPrefix', '', 'abc', True), |
||||
('BothEmpty', '', '', True)) |
||||
def testStartsWith(self, prefix, string, result): |
||||
self.assertEquals(result, strings.startswith(prefix)) |
||||
|
||||
Named tests also have the benefit that they can be run individually |
||||
from the command line: |
||||
|
||||
$ testmodule.py NamedExample.testStartsWithNormal |
||||
. |
||||
-------------------------------------------------------------------- |
||||
Ran 1 test in 0.000s |
||||
|
||||
OK |
||||
|
||||
Parameterized Classes |
||||
===================== |
||||
If invocation arguments are shared across test methods in a single |
||||
ParameterizedTestCase class, instead of decorating all test methods |
||||
individually, the class itself can be decorated: |
||||
|
||||
@parameterized.Parameters( |
||||
(1, 2, 3) |
||||
(4, 5, 9)) |
||||
class ArithmeticTest(parameterized.ParameterizedTestCase): |
||||
def testAdd(self, arg1, arg2, result): |
||||
self.assertEqual(arg1 + arg2, result) |
||||
|
||||
def testSubtract(self, arg2, arg2, result): |
||||
self.assertEqual(result - arg1, arg2) |
||||
|
||||
Inputs from Iterables |
||||
===================== |
||||
If parameters should be shared across several test cases, or are dynamically |
||||
created from other sources, a single non-tuple iterable can be passed into |
||||
the decorator. This iterable will be used to obtain the test cases: |
||||
|
||||
class AdditionExample(parameterized.ParameterizedTestCase): |
||||
@parameterized.Parameters( |
||||
c.op1, c.op2, c.result for c in testcases |
||||
) |
||||
def testAddition(self, op1, op2, result): |
||||
self.assertEquals(result, op1 + op2) |
||||
|
||||
|
||||
Single-Argument Test Methods |
||||
============================ |
||||
If a test method takes only one argument, the single argument does not need to |
||||
be wrapped into a tuple: |
||||
|
||||
class NegativeNumberExample(parameterized.ParameterizedTestCase): |
||||
@parameterized.Parameters( |
||||
-1, -3, -4, -5 |
||||
) |
||||
def testIsNegative(self, arg): |
||||
self.assertTrue(IsNegative(arg)) |
||||
""" |
||||
|
||||
__author__ = 'tmarek@google.com (Torsten Marek)' |
||||
|
||||
import collections |
||||
import functools |
||||
import re |
||||
import types |
||||
import unittest |
||||
import uuid |
||||
|
||||
from google.apputils import basetest |
||||
|
||||
ADDR_RE = re.compile(r'\<([a-zA-Z0-9_\-\.]+) object at 0x[a-fA-F0-9]+\>') |
||||
_SEPARATOR = uuid.uuid1().hex |
||||
_FIRST_ARG = object() |
||||
_ARGUMENT_REPR = object() |
||||
|
||||
|
||||
def _CleanRepr(obj): |
||||
return ADDR_RE.sub(r'<\1>', repr(obj)) |
||||
|
||||
|
||||
# Helper function formerly from the unittest module, removed from it in |
||||
# Python 2.7. |
||||
def _StrClass(cls): |
||||
return '%s.%s' % (cls.__module__, cls.__name__) |
||||
|
||||
|
||||
def _NonStringIterable(obj): |
||||
return (isinstance(obj, collections.Iterable) and not |
||||
isinstance(obj, basestring)) |
||||
|
||||
|
||||
def _FormatParameterList(testcase_params): |
||||
if isinstance(testcase_params, collections.Mapping): |
||||
return ', '.join('%s=%s' % (argname, _CleanRepr(value)) |
||||
for argname, value in testcase_params.iteritems()) |
||||
elif _NonStringIterable(testcase_params): |
||||
return ', '.join(map(_CleanRepr, testcase_params)) |
||||
else: |
||||
return _FormatParameterList((testcase_params,)) |
||||
|
||||
|
||||
class _ParameterizedTestIter(object): |
||||
"""Callable and iterable class for producing new test cases.""" |
||||
|
||||
def __init__(self, test_method, testcases, naming_type): |
||||
"""Returns concrete test functions for a test and a list of parameters. |
||||
|
||||
The naming_type is used to determine the name of the concrete |
||||
functions as reported by the unittest framework. If naming_type is |
||||
_FIRST_ARG, the testcases must be tuples, and the first element must |
||||
have a string representation that is a valid Python identifier. |
||||
|
||||
Args: |
||||
test_method: The decorated test method. |
||||
testcases: (list of tuple/dict) A list of parameter |
||||
tuples/dicts for individual test invocations. |
||||
naming_type: The test naming type, either _NAMED or _ARGUMENT_REPR. |
||||
""" |
||||
self._test_method = test_method |
||||
self.testcases = testcases |
||||
self._naming_type = naming_type |
||||
|
||||
def __call__(self, *args, **kwargs): |
||||
raise RuntimeError('You appear to be running a parameterized test case ' |
||||
'without having inherited from parameterized.' |
||||
'ParameterizedTestCase. This is bad because none of ' |
||||
'your test cases are actually being run.') |
||||
|
||||
def __iter__(self): |
||||
test_method = self._test_method |
||||
naming_type = self._naming_type |
||||
|
||||
def MakeBoundParamTest(testcase_params): |
||||
@functools.wraps(test_method) |
||||
def BoundParamTest(self): |
||||
if isinstance(testcase_params, collections.Mapping): |
||||
test_method(self, **testcase_params) |
||||
elif _NonStringIterable(testcase_params): |
||||
test_method(self, *testcase_params) |
||||
else: |
||||
test_method(self, testcase_params) |
||||
|
||||
if naming_type is _FIRST_ARG: |
||||
# Signal the metaclass that the name of the test function is unique |
||||
# and descriptive. |
||||
BoundParamTest.__x_use_name__ = True |
||||
BoundParamTest.__name__ += str(testcase_params[0]) |
||||
testcase_params = testcase_params[1:] |
||||
elif naming_type is _ARGUMENT_REPR: |
||||
# __x_extra_id__ is used to pass naming information to the __new__ |
||||
# method of TestGeneratorMetaclass. |
||||
# The metaclass will make sure to create a unique, but nondescriptive |
||||
# name for this test. |
||||
BoundParamTest.__x_extra_id__ = '(%s)' % ( |
||||
_FormatParameterList(testcase_params),) |
||||
else: |
||||
raise RuntimeError('%s is not a valid naming type.' % (naming_type,)) |
||||
|
||||
BoundParamTest.__doc__ = '%s(%s)' % ( |
||||
BoundParamTest.__name__, _FormatParameterList(testcase_params)) |
||||
if test_method.__doc__: |
||||
BoundParamTest.__doc__ += '\n%s' % (test_method.__doc__,) |
||||
return BoundParamTest |
||||
return (MakeBoundParamTest(c) for c in self.testcases) |
||||
|
||||
|
||||
def _IsSingletonList(testcases): |
||||
"""True iff testcases contains only a single non-tuple element.""" |
||||
return len(testcases) == 1 and not isinstance(testcases[0], tuple) |
||||
|
||||
|
||||
def _ModifyClass(class_object, testcases, naming_type): |
||||
assert not getattr(class_object, '_id_suffix', None), ( |
||||
'Cannot add parameters to %s,' |
||||
' which already has parameterized methods.' % (class_object,)) |
||||
class_object._id_suffix = id_suffix = {} |
||||
for name, obj in class_object.__dict__.items(): |
||||
if (name.startswith(unittest.TestLoader.testMethodPrefix) |
||||
and isinstance(obj, types.FunctionType)): |
||||
delattr(class_object, name) |
||||
methods = {} |
||||
_UpdateClassDictForParamTestCase( |
||||
methods, id_suffix, name, |
||||
_ParameterizedTestIter(obj, testcases, naming_type)) |
||||
for name, meth in methods.iteritems(): |
||||
setattr(class_object, name, meth) |
||||
|
||||
|
||||
def _ParameterDecorator(naming_type, testcases): |
||||
"""Implementation of the parameterization decorators. |
||||
|
||||
Args: |
||||
naming_type: The naming type. |
||||
testcases: Testcase parameters. |
||||
|
||||
Returns: |
||||
A function for modifying the decorated object. |
||||
""" |
||||
def _Apply(obj): |
||||
if isinstance(obj, type): |
||||
_ModifyClass( |
||||
obj, |
||||
list(testcases) if not isinstance(testcases, collections.Sequence) |
||||
else testcases, |
||||
naming_type) |
||||
return obj |
||||
else: |
||||
return _ParameterizedTestIter(obj, testcases, naming_type) |
||||
|
||||
if _IsSingletonList(testcases): |
||||
assert _NonStringIterable(testcases[0]), ( |
||||
'Single parameter argument must be a non-string iterable') |
||||
testcases = testcases[0] |
||||
|
||||
return _Apply |
||||
|
||||
|
||||
def Parameters(*testcases): |
||||
"""A decorator for creating parameterized tests. |
||||
|
||||
See the module docstring for a usage example. |
||||
Args: |
||||
*testcases: Parameters for the decorated method, either a single |
||||
iterable, or a list of tuples/dicts/objects (for tests |
||||
with only one argument). |
||||
|
||||
Returns: |
||||
A test generator to be handled by TestGeneratorMetaclass. |
||||
""" |
||||
return _ParameterDecorator(_ARGUMENT_REPR, testcases) |
||||
|
||||
|
||||
def NamedParameters(*testcases): |
||||
"""A decorator for creating parameterized tests. |
||||
|
||||
See the module docstring for a usage example. The first element of |
||||
each parameter tuple should be a string and will be appended to the |
||||
name of the test method. |
||||
|
||||
Args: |
||||
*testcases: Parameters for the decorated method, either a single |
||||
iterable, or a list of tuples. |
||||
|
||||
Returns: |
||||
A test generator to be handled by TestGeneratorMetaclass. |
||||
""" |
||||
return _ParameterDecorator(_FIRST_ARG, testcases) |
||||
|
||||
|
||||
class TestGeneratorMetaclass(type): |
||||
"""Metaclass for test cases with test generators. |
||||
|
||||
A test generator is an iterable in a testcase that produces callables. These |
||||
callables must be single-argument methods. These methods are injected into |
||||
the class namespace and the original iterable is removed. If the name of the |
||||
iterable conforms to the test pattern, the injected methods will be picked |
||||
up as tests by the unittest framework. |
||||
|
||||
In general, it is supposed to be used in conjuction with the |
||||
Parameters decorator. |
||||
""" |
||||
|
||||
def __new__(mcs, class_name, bases, dct): |
||||
dct['_id_suffix'] = id_suffix = {} |
||||
for name, obj in dct.items(): |
||||
if (name.startswith(unittest.TestLoader.testMethodPrefix) and |
||||
_NonStringIterable(obj)): |
||||
iterator = iter(obj) |
||||
dct.pop(name) |
||||
_UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator) |
||||
|
||||
return type.__new__(mcs, class_name, bases, dct) |
||||
|
||||
|
||||
def _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator): |
||||
"""Adds individual test cases to a dictionary. |
||||
|
||||
Args: |
||||
dct: The target dictionary. |
||||
id_suffix: The dictionary for mapping names to test IDs. |
||||
name: The original name of the test case. |
||||
iterator: The iterator generating the individual test cases. |
||||
""" |
||||
for idx, func in enumerate(iterator): |
||||
assert callable(func), 'Test generators must yield callables, got %r' % ( |
||||
func,) |
||||
if getattr(func, '__x_use_name__', False): |
||||
new_name = func.__name__ |
||||
else: |
||||
new_name = '%s%s%d' % (name, _SEPARATOR, idx) |
||||
assert new_name not in dct, ( |
||||
'Name of parameterized test case "%s" not unique' % (new_name,)) |
||||
dct[new_name] = func |
||||
id_suffix[new_name] = getattr(func, '__x_extra_id__', '') |
||||
|
||||
|
||||
class ParameterizedTestCase(basetest.TestCase): |
||||
"""Base class for test cases using the Parameters decorator.""" |
||||
__metaclass__ = TestGeneratorMetaclass |
||||
|
||||
def _OriginalName(self): |
||||
return self._testMethodName.split(_SEPARATOR)[0] |
||||
|
||||
def __str__(self): |
||||
return '%s (%s)' % (self._OriginalName(), _StrClass(self.__class__)) |
||||
|
||||
def id(self): # pylint: disable=invalid-name |
||||
"""Returns the descriptive ID of the test. |
||||
|
||||
This is used internally by the unittesting framework to get a name |
||||
for the test to be used in reports. |
||||
|
||||
Returns: |
||||
The test id. |
||||
""" |
||||
return '%s.%s%s' % (_StrClass(self.__class__), |
||||
self._OriginalName(), |
||||
self._id_suffix.get(self._testMethodName, '')) |
||||
|
||||
|
||||
def CoopParameterizedTestCase(other_base_class): |
||||
"""Returns a new base class with a cooperative metaclass base. |
||||
|
||||
This enables the ParameterizedTestCase to be used in combination |
||||
with other base classes that have custom metaclasses, such as |
||||
mox.MoxTestBase. |
||||
|
||||
Only works with metaclasses that do not override type.__new__. |
||||
|
||||
Example: |
||||
|
||||
import google3 |
||||
import mox |
||||
|
||||
from google3.testing.pybase import parameterized |
||||
|
||||
class ExampleTest(parameterized.CoopParameterizedTestCase(mox.MoxTestBase)): |
||||
... |
||||
|
||||
Args: |
||||
other_base_class: (class) A test case base class. |
||||
|
||||
Returns: |
||||
A new class object. |
||||
""" |
||||
metaclass = type( |
||||
'CoopMetaclass', |
||||
(other_base_class.__metaclass__, |
||||
TestGeneratorMetaclass), {}) |
||||
return metaclass( |
||||
'CoopParameterizedTestCase', |
||||
(other_base_class, ParameterizedTestCase), {}) |
Loading…
Reference in new issue