153 lines
5.2 KiB
Python
153 lines
5.2 KiB
Python
# Copyright 2015 gRPC authors.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
from __future__ import absolute_import
|
|
|
|
import importlib
|
|
import logging
|
|
import os
|
|
import pkgutil
|
|
import re
|
|
import sys
|
|
import unittest
|
|
|
|
import coverage
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
TEST_MODULE_REGEX = r"^.*_test$"
|
|
|
|
|
|
# Determines the path og a given path relative to the first matching
|
|
# path on sys.path. Useful for determining what a directory's module
|
|
# path will be.
|
|
def _relativize_to_sys_path(path):
|
|
for sys_path in sys.path:
|
|
if path.startswith(sys_path):
|
|
relative = path[len(sys_path) :]
|
|
if not relative:
|
|
return ""
|
|
if relative.startswith(os.path.sep):
|
|
relative = relative[len(os.path.sep) :]
|
|
if not relative.endswith(os.path.sep):
|
|
relative += os.path.sep
|
|
return relative
|
|
raise AssertionError("Failed to relativize {} to sys.path.".format(path))
|
|
|
|
|
|
def _relative_path_to_module_prefix(path):
|
|
return path.replace(os.path.sep, ".")
|
|
|
|
|
|
class Loader(object):
|
|
"""Test loader for setuptools test suite support.
|
|
|
|
Attributes:
|
|
suite (unittest.TestSuite): All tests collected by the loader.
|
|
loader (unittest.TestLoader): Standard Python unittest loader to be ran per
|
|
module discovered.
|
|
module_matcher (re.RegexObject): A regular expression object to match
|
|
against module names and determine whether or not the discovered module
|
|
contributes to the test suite.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.suite = unittest.TestSuite()
|
|
self.loader = unittest.TestLoader()
|
|
self.module_matcher = re.compile(TEST_MODULE_REGEX)
|
|
|
|
def loadTestsFromNames(self, names, module=None):
|
|
"""Function mirroring TestLoader::loadTestsFromNames, as expected by
|
|
setuptools.setup argument `test_loader`."""
|
|
# ensure that we capture decorators and definitions (else our coverage
|
|
# measure unnecessarily suffers)
|
|
coverage_context = coverage.Coverage(data_suffix=True)
|
|
coverage_context.start()
|
|
imported_modules = tuple(
|
|
importlib.import_module(name) for name in names
|
|
)
|
|
for imported_module in imported_modules:
|
|
self.visit_module(imported_module)
|
|
for imported_module in imported_modules:
|
|
try:
|
|
package_paths = imported_module.__path__
|
|
except AttributeError:
|
|
continue
|
|
self.walk_packages(package_paths)
|
|
coverage_context.stop()
|
|
coverage_context.save()
|
|
return self.suite
|
|
|
|
def walk_packages(self, package_paths):
|
|
"""Walks over the packages, dispatching `visit_module` calls.
|
|
|
|
Args:
|
|
package_paths (list): A list of paths over which to walk through modules
|
|
along.
|
|
"""
|
|
for path in package_paths:
|
|
self._walk_package(path)
|
|
|
|
def _walk_package(self, package_path):
|
|
prefix = _relative_path_to_module_prefix(
|
|
_relativize_to_sys_path(package_path)
|
|
)
|
|
for importer, module_name, is_package in pkgutil.walk_packages(
|
|
[package_path], prefix
|
|
):
|
|
module = None
|
|
if module_name in sys.modules:
|
|
module = sys.modules[module_name]
|
|
self.visit_module(module)
|
|
else:
|
|
try:
|
|
spec = importer.find_spec(module_name)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
self.visit_module(module)
|
|
except ModuleNotFoundError:
|
|
logger.debug("Skip loading %s", module_name)
|
|
|
|
def visit_module(self, module):
|
|
"""Visits the module, adding discovered tests to the test suite.
|
|
|
|
Args:
|
|
module (module): Module to match against self.module_matcher; if matched
|
|
it has its tests loaded via self.loader into self.suite.
|
|
"""
|
|
if self.module_matcher.match(module.__name__):
|
|
module_suite = self.loader.loadTestsFromModule(module)
|
|
self.suite.addTest(module_suite)
|
|
|
|
|
|
def iterate_suite_cases(suite):
|
|
"""Generator over all unittest.TestCases in a unittest.TestSuite.
|
|
|
|
Args:
|
|
suite (unittest.TestSuite): Suite to iterate over in the generator.
|
|
|
|
Returns:
|
|
generator: A generator over all unittest.TestCases in `suite`.
|
|
"""
|
|
for item in suite:
|
|
if isinstance(item, unittest.TestSuite):
|
|
for child_item in iterate_suite_cases(item):
|
|
yield child_item
|
|
elif isinstance(item, unittest.TestCase):
|
|
yield item
|
|
else:
|
|
raise ValueError(
|
|
"unexpected suite item of type {}".format(type(item))
|
|
)
|