Commit e1d8b7ba authored by Dom Sekotill's avatar Dom Sekotill
Browse files

Initial commit

parents
Loading
Loading
Loading
Loading

.gitignore

0 → 100644
+2 −0
Original line number Diff line number Diff line
# python generated
*.py[cod]
+22 −0
Original line number Diff line number Diff line
#
# This file is part of 'py-unittest-extra'
# Copyright (C) 2015  Dom Sekotill
#
# Permission is hereby granted, free of charge, to any person obtaining a copy 
# of this software and associated documentation files (the "Software"), to deal 
# in the Software without restriction, including without limitation the rights 
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
# copies of the Software, and to permit persons to whom the Software is 
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in 
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
# SOFTWARE.
#
+137 −0
Original line number Diff line number Diff line
#
# This file is part of 'py-unittest-extra'
# Copyright (C) 2015  Dom Sekotill
#
# Permission is hereby granted, free of charge, to any person obtaining a copy 
# of this software and associated documentation files (the "Software"), to deal 
# in the Software without restriction, including without limitation the rights 
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
# copies of the Software, and to permit persons to whom the Software is 
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in 
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
# SOFTWARE.
#

"""
A module to produce coverage reports for testing.
"""

from six import string_types
from importlib import import_module
from pkgutil import walk_packages
from itertools import product
from functools import wraps
import contextlib
import itertools
import logging
import sys


@contextlib.contextmanager
def coverage(modules, include=None, exclude=None, file=None):
	"""
	Return a context manager that generates a coverage report for 'modules'

	'include', 'exclude' and 'file' are as the arguments to
	coverage.coverage.report().
	"""

	try:
		from coverage import coverage as Coverage
	except ImportError:
		logging.warning(
			"The code coverage module 'coverage' was not found in the "
			"search paths; there will be no coverage report."
		)
		yield
		return

	old_modules = sys.modules.copy()
	modules = {import_module(m) if isinstance(m, string_types) else m for m in modules}

	for module in list(modules):
		try:
			walker = walk_packages(module.__path__, module.__name__+'.')
		except (AttributeError):
			modules.add(module)
		else:
			modules.update(import_module(n) for l,n,p in walker)

	for module in modules:
		del sys.modules[module.__name__]

	cover = Coverage()
	cover.start()

	try:
		yield
	finally:
		sys.modules.clear()
		sys.modules.update(old_modules)

		cover.stop()
		cover.save()

		with (file or sys.stdout) as file:
			if file.isatty():
				file.write("\nCoverage Report:\n================\n")
			cover.report(list(modules), include=include, omit=exclude, file=file)


def test_coverage_command(superclass, packages=None):
	"""
	Generate a distutils command class which is a subclass of 'superclass'.

	'superclass' should be a testing command (eg. setuptools.commands.test.test)
	for which a coverage report will be generated at the end of a call to the
	run() method.

	'packages' should be a list of packages to generate the report for; it
	defaults to the value of 'packages' passed to setup().

	Run 'python setup.py <command> --help' to see the added command line
	arguments.
	"""

	class test(superclass):
		__doc__ = superclass.__doc__

		user_options = superclass.user_options + [
			('with-coverage', None,
				"Include a coverage report (if the coverage module is installed)"),
			('coverage-report=', None,
				"Write the coverage report to FILE (implies --with-coverage)"),
		]

		@wraps(superclass.initialize_options)
		def initialize_options(self):
			super(test, self).initialize_options()
			self.with_coverage = False
			self.coverage_report = None

		@wraps(superclass.finalize_options)
		def finalize_options(self):
			super(test, self).finalize_options()
			if self.coverage_report:
				self.with_coverage = True
				self.coverage_report = open(self.coverage_report, 'w')

		@wraps(superclass.run)
		def run(self):
			if not self.with_coverage:
				return super(test, self).run()

			with coverage(packages or self.distribution.packages,
					file=self.coverage_report):
				super(test, self).run()

	return test
+151 −0
Original line number Diff line number Diff line
#
# This file is part of 'py-unittest-extra'
# Copyright (C) 2015  Dom Sekotill
#
# Permission is hereby granted, free of charge, to any person obtaining a copy 
# of this software and associated documentation files (the "Software"), to deal 
# in the Software without restriction, including without limitation the rights 
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
# copies of the Software, and to permit persons to whom the Software is 
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in 
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
# SOFTWARE.
#

"""
A module to increase test re-use, by running tests with multiple argument sets.
"""

import unittest
from collections import Iterable
from functools import wraps
from six import string_types


class test_generator(object):
	"""
	A class which wraps a test and calls it with different arguments.

	Used as a decorator on a test function, an instance of this class will be
	used by the TestGeneratorMeta class to generate multiple wrapped tests,
	each being called with a set of arguments passed to the constructor.

	The constructor takes one argument, which is either an iterable of arguments
	or a string giving the attribute name of an iterable of arguments in the
	class. The arguments themselves must be either:
	 1) An iterable of positional arguments, or
	 2) A mapping of keyword arguments, or
	 3) A 2-item object where item 0 matches (1) and item 1 matches (2)

	Examples:

	  ```
	  class TestCase(unittest.TestCase, metaclass=TestGeneratorMeta):

	      @test_generator([('foo', 'bar'), (1, 2)])
	      def some_test(self, a, b):
	          # run test with a & b...
	  ```

	  ```
	  class TestCase(unittest.TestCase, metaclass=TestGeneratorMeta):

	      some_test_args = [
	          ('foo', 'bar'),
	          (1, 2),
	      ]

	      @test_generator('some_test_args')
	      def some_test(self, a, b):
	          # run test with a & b...
	  ```

	  ```
	  class TestCase(unittest.TestCase, metaclass=TestGeneratorMeta):

	      some_test_args = [
	          (('foo', 'bar'), {'type': str}),
	          (1, 2),
	      ]

	      @test_generator('some_test_args')
	      def some_test(self, a, b, type=None):
	          # run test with a & b...
	  ```
	"""

	def __init__(self, values):
		self.values = values

	def __call__(self, func):
		self.func = func
		return self

	def wrap(self, attr):
		"""
		Generate a test-wrapper with each set of arguments and add it to 'attr'.
		"""
		if isinstance(self.values, string_types):
			values = attr[self.values]
		else:
			values = self.values
		for name, func in self._wrap(values):
			attr[name] = func

	def _wrap(self, value_list):
		for number, values in enumerate(value_list):
			if self._is_args_kwds(values):
				args, kwds = values
			elif self._is_args(values):
				args = values
				kwds = {}
			elif self._is_kwds(values):
				kwds = values
				args = []
			else:
				args = values,
				kwds = {}
			wrapper = self._make_wrap(args, kwds)
			yield 'test_{0}_{1}'.format(wrapper.__name__, number), wrapper

		yield self.func.__name__, self.func
		yield '_test_generator_' + self.func.__name__, self

	def _make_wrap(self, args, kwds):
		@wraps(self.func)
		def wrapper(*a):
			return self.func(*a + args, **kwds)
		return wrapper

	@staticmethod
	def _is_args(obj):
		return isinstance(obj, Iterable) and not isinstance(obj, string_types)

	@staticmethod
	def _is_kwds(obj):
		return isinstance(obj, dict)

	@classmethod
	def _is_args_kwds(cls, obj):
		try:
			return len(obj) == 2 and cls._is_args(obj[0]) and cls._is_args(obj[1])
		except (TypeError):
			return False


class TestGeneratorMeta(type):

	def __new__(cls, clsname, bases, attr):
		for name, obj in list(attr.items()):
			if isinstance(obj, test_generator):
				obj.wrap(attr)
		return super(TestGeneratorMeta).__new__(cls, clsname, bases, attr)