mirror of https://github.com/python/cpython
401 lines
13 KiB
Python
401 lines
13 KiB
Python
"""Support code for packaging test cases.
|
|
|
|
*This module should not be considered public: its content and API may
|
|
change in incompatible ways.*
|
|
|
|
A few helper classes are provided: LoggingCatcher, TempdirManager and
|
|
EnvironRestorer. They are written to be used as mixins::
|
|
|
|
from packaging.tests import unittest
|
|
from packaging.tests.support import LoggingCatcher
|
|
|
|
class SomeTestCase(LoggingCatcher, unittest.TestCase):
|
|
...
|
|
|
|
If you need to define a setUp method on your test class, you have to
|
|
call the mixin class' setUp method or it won't work (same thing for
|
|
tearDown):
|
|
|
|
def setUp(self):
|
|
super(SomeTestCase, self).setUp()
|
|
... # other setup code
|
|
|
|
Also provided is a DummyCommand class, useful to mock commands in the
|
|
tests of another command that needs them, for example to fake
|
|
compilation in build_ext (this requires that the mock build_ext command
|
|
be injected into the distribution object's command_obj dictionary).
|
|
|
|
For tests that need to compile an extension module, use the
|
|
copy_xxmodule_c and fixup_build_ext functions.
|
|
|
|
Each class or function has a docstring to explain its purpose and usage.
|
|
Existing tests should also be used as examples.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import shutil
|
|
import logging
|
|
import weakref
|
|
import tempfile
|
|
import sysconfig
|
|
|
|
from packaging.dist import Distribution
|
|
from packaging.util import resolve_name
|
|
from packaging.command import set_command, _COMMANDS
|
|
|
|
from packaging.tests import unittest
|
|
from test.support import requires_zlib, unlink
|
|
|
|
# define __all__ to make pydoc more useful
|
|
__all__ = [
|
|
# TestCase mixins
|
|
'LoggingCatcher', 'TempdirManager', 'EnvironRestorer',
|
|
# mocks
|
|
'DummyCommand', 'TestDistribution', 'Inputs',
|
|
# misc. functions and decorators
|
|
'fake_dec', 'create_distribution', 'use_command',
|
|
'copy_xxmodule_c', 'fixup_build_ext',
|
|
'skip_2to3_optimize',
|
|
# imported from this module for backport purposes
|
|
'unittest', 'requires_zlib', 'skip_unless_symlink',
|
|
]
|
|
|
|
|
|
logger = logging.getLogger('packaging')
|
|
logger2to3 = logging.getLogger('RefactoringTool')
|
|
|
|
|
|
class _TestHandler(logging.handlers.BufferingHandler):
|
|
# stolen and adapted from test.support
|
|
|
|
def __init__(self):
|
|
super(_TestHandler, self).__init__(0)
|
|
self.setLevel(logging.DEBUG)
|
|
|
|
def shouldFlush(self):
|
|
return False
|
|
|
|
def emit(self, record):
|
|
self.buffer.append(record)
|
|
|
|
|
|
class LoggingCatcher:
|
|
"""TestCase-compatible mixin to receive logging calls.
|
|
|
|
Upon setUp, instances of this classes get a BufferingHandler that's
|
|
configured to record all messages logged to the 'packaging' logger.
|
|
|
|
Use get_logs to retrieve messages and self.loghandler.flush to discard
|
|
them. get_logs automatically flushes the logs, unless you pass
|
|
*flush=False*, for example to make multiple calls to the method with
|
|
different level arguments. If your test calls some code that generates
|
|
logging message and then you don't call get_logs, you will need to flush
|
|
manually before testing other code in the same test_* method, otherwise
|
|
get_logs in the next lines will see messages from the previous lines.
|
|
See example in test_command_check.
|
|
"""
|
|
|
|
def setUp(self):
|
|
super(LoggingCatcher, self).setUp()
|
|
self.loghandler = handler = _TestHandler()
|
|
self._old_levels = logger.level, logger2to3.level
|
|
logger.addHandler(handler)
|
|
logger.setLevel(logging.DEBUG) # we want all messages
|
|
logger2to3.setLevel(logging.CRITICAL) # we don't want 2to3 messages
|
|
|
|
def tearDown(self):
|
|
handler = self.loghandler
|
|
# All this is necessary to properly shut down the logging system and
|
|
# avoid a regrtest complaint. Thanks to Vinay Sajip for the help.
|
|
handler.close()
|
|
logger.removeHandler(handler)
|
|
for ref in weakref.getweakrefs(handler):
|
|
logging._removeHandlerRef(ref)
|
|
del self.loghandler
|
|
logger.setLevel(self._old_levels[0])
|
|
logger2to3.setLevel(self._old_levels[1])
|
|
super(LoggingCatcher, self).tearDown()
|
|
|
|
def get_logs(self, level=logging.WARNING, flush=True):
|
|
"""Return all log messages with given level.
|
|
|
|
*level* defaults to logging.WARNING.
|
|
|
|
For log calls with arguments (i.e. logger.info('bla bla %r', arg)),
|
|
the messages will be formatted before being returned (e.g. "bla bla
|
|
'thing'").
|
|
|
|
Returns a list. Automatically flushes the loghandler after being
|
|
called, unless *flush* is False (this is useful to get e.g. all
|
|
warnings then all info messages).
|
|
"""
|
|
messages = [log.getMessage() for log in self.loghandler.buffer
|
|
if log.levelno == level]
|
|
if flush:
|
|
self.loghandler.flush()
|
|
return messages
|
|
|
|
|
|
class TempdirManager:
|
|
"""TestCase-compatible mixin to create temporary directories and files.
|
|
|
|
Directories and files created in a test_* method will be removed after it
|
|
has run.
|
|
"""
|
|
|
|
def setUp(self):
|
|
super(TempdirManager, self).setUp()
|
|
self._olddir = os.getcwd()
|
|
self._basetempdir = tempfile.mkdtemp()
|
|
self._files = []
|
|
|
|
def tearDown(self):
|
|
for handle, name in self._files:
|
|
handle.close()
|
|
unlink(name)
|
|
|
|
os.chdir(self._olddir)
|
|
shutil.rmtree(self._basetempdir)
|
|
super(TempdirManager, self).tearDown()
|
|
|
|
def mktempfile(self):
|
|
"""Create a read-write temporary file and return it."""
|
|
fd, fn = tempfile.mkstemp(dir=self._basetempdir)
|
|
os.close(fd)
|
|
fp = open(fn, 'w+')
|
|
self._files.append((fp, fn))
|
|
return fp
|
|
|
|
def mkdtemp(self):
|
|
"""Create a temporary directory and return its path."""
|
|
d = tempfile.mkdtemp(dir=self._basetempdir)
|
|
return d
|
|
|
|
def write_file(self, path, content='xxx', encoding=None):
|
|
"""Write a file at the given path.
|
|
|
|
path can be a string, a tuple or a list; if it's a tuple or list,
|
|
os.path.join will be used to produce a path.
|
|
"""
|
|
if isinstance(path, (list, tuple)):
|
|
path = os.path.join(*path)
|
|
with open(path, 'w', encoding=encoding) as f:
|
|
f.write(content)
|
|
|
|
def create_dist(self, **kw):
|
|
"""Create a stub distribution object and files.
|
|
|
|
This function creates a Distribution instance (use keyword arguments
|
|
to customize it) and a temporary directory with a project structure
|
|
(currently an empty directory).
|
|
|
|
It returns the path to the directory and the Distribution instance.
|
|
You can use self.write_file to write any file in that
|
|
directory, e.g. setup scripts or Python modules.
|
|
"""
|
|
if 'name' not in kw:
|
|
kw['name'] = 'foo'
|
|
tmp_dir = self.mkdtemp()
|
|
project_dir = os.path.join(tmp_dir, kw['name'])
|
|
os.mkdir(project_dir)
|
|
dist = Distribution(attrs=kw)
|
|
return project_dir, dist
|
|
|
|
def assertIsFile(self, *args):
|
|
path = os.path.join(*args)
|
|
dirname = os.path.dirname(path)
|
|
file = os.path.basename(path)
|
|
if os.path.isdir(dirname):
|
|
files = os.listdir(dirname)
|
|
msg = "%s not found in %s: %s" % (file, dirname, files)
|
|
assert os.path.isfile(path), msg
|
|
else:
|
|
raise AssertionError(
|
|
'%s not found. %s does not exist' % (file, dirname))
|
|
|
|
def assertIsNotFile(self, *args):
|
|
path = os.path.join(*args)
|
|
self.assertFalse(os.path.isfile(path), "%r exists" % path)
|
|
|
|
|
|
class EnvironRestorer:
|
|
"""TestCase-compatible mixin to restore or delete environment variables.
|
|
|
|
The variables to restore (or delete if they were not originally present)
|
|
must be explicitly listed in self.restore_environ. It's better to be
|
|
aware of what we're modifying instead of saving and restoring the whole
|
|
environment.
|
|
"""
|
|
|
|
def setUp(self):
|
|
super(EnvironRestorer, self).setUp()
|
|
self._saved = []
|
|
self._added = []
|
|
for key in self.restore_environ:
|
|
if key in os.environ:
|
|
self._saved.append((key, os.environ[key]))
|
|
else:
|
|
self._added.append(key)
|
|
|
|
def tearDown(self):
|
|
for key, value in self._saved:
|
|
os.environ[key] = value
|
|
for key in self._added:
|
|
os.environ.pop(key, None)
|
|
super(EnvironRestorer, self).tearDown()
|
|
|
|
|
|
class DummyCommand:
|
|
"""Class to store options for retrieval via set_undefined_options().
|
|
|
|
Useful for mocking one dependency command in the tests for another
|
|
command, see e.g. the dummy build command in test_build_scripts.
|
|
"""
|
|
# XXX does not work with dist.reinitialize_command, which typechecks
|
|
# and wants a finalized attribute
|
|
|
|
def __init__(self, **kwargs):
|
|
for kw, val in kwargs.items():
|
|
setattr(self, kw, val)
|
|
|
|
def ensure_finalized(self):
|
|
pass
|
|
|
|
|
|
class TestDistribution(Distribution):
|
|
"""Distribution subclasses that avoids the default search for
|
|
configuration files.
|
|
|
|
The ._config_files attribute must be set before
|
|
.parse_config_files() is called.
|
|
"""
|
|
|
|
def find_config_files(self):
|
|
return self._config_files
|
|
|
|
|
|
class Inputs:
|
|
"""Fakes user inputs."""
|
|
# TODO document usage
|
|
# TODO use context manager or something for auto cleanup
|
|
|
|
def __init__(self, *answers):
|
|
self.answers = answers
|
|
self.index = 0
|
|
|
|
def __call__(self, prompt=''):
|
|
try:
|
|
return self.answers[self.index]
|
|
finally:
|
|
self.index += 1
|
|
|
|
|
|
def create_distribution(configfiles=()):
|
|
"""Prepares a distribution with given config files parsed."""
|
|
d = TestDistribution()
|
|
d.config.find_config_files = d.find_config_files
|
|
d._config_files = configfiles
|
|
d.parse_config_files()
|
|
d.parse_command_line()
|
|
return d
|
|
|
|
|
|
def use_command(testcase, fullname):
|
|
"""Register command at *fullname* for the duration of a test."""
|
|
set_command(fullname)
|
|
# XXX maybe set_command should return the class object
|
|
name = resolve_name(fullname).get_command_name()
|
|
# XXX maybe we need a public API to remove commands
|
|
testcase.addCleanup(_COMMANDS.__delitem__, name)
|
|
|
|
|
|
def fake_dec(*args, **kw):
|
|
"""Fake decorator"""
|
|
def _wrap(func):
|
|
def __wrap(*args, **kw):
|
|
return func(*args, **kw)
|
|
return __wrap
|
|
return _wrap
|
|
|
|
|
|
def copy_xxmodule_c(directory):
|
|
"""Helper for tests that need the xxmodule.c source file.
|
|
|
|
Example use:
|
|
|
|
def test_compile(self):
|
|
copy_xxmodule_c(self.tmpdir)
|
|
self.assertIn('xxmodule.c', os.listdir(self.tmpdir))
|
|
|
|
If the source file can be found, it will be copied to *directory*. If not,
|
|
the test will be skipped. Errors during copy are not caught.
|
|
"""
|
|
filename = _get_xxmodule_path()
|
|
if filename is None:
|
|
raise unittest.SkipTest('cannot find xxmodule.c')
|
|
shutil.copy(filename, directory)
|
|
|
|
|
|
def _get_xxmodule_path():
|
|
if sysconfig.is_python_build():
|
|
srcdir = sysconfig.get_config_var('projectbase')
|
|
path = os.path.join(os.getcwd(), srcdir, 'Modules', 'xxmodule.c')
|
|
else:
|
|
path = os.path.join(os.path.dirname(__file__), 'xxmodule.c')
|
|
if os.path.exists(path):
|
|
return path
|
|
|
|
|
|
def fixup_build_ext(cmd):
|
|
"""Function needed to make build_ext tests pass.
|
|
|
|
When Python was built with --enable-shared on Unix, -L. is not enough to
|
|
find libpython<blah>.so, because regrtest runs in a tempdir, not in the
|
|
source directory where the .so lives. (Mac OS X embeds absolute paths
|
|
to shared libraries into executables, so the fixup is a no-op on that
|
|
platform.)
|
|
|
|
When Python was built with in debug mode on Windows, build_ext commands
|
|
need their debug attribute set, and it is not done automatically for
|
|
some reason.
|
|
|
|
This function handles both of these things, and also fixes
|
|
cmd.distribution.include_dirs if the running Python is an uninstalled
|
|
build. Example use:
|
|
|
|
cmd = build_ext(dist)
|
|
support.fixup_build_ext(cmd)
|
|
cmd.ensure_finalized()
|
|
"""
|
|
if os.name == 'nt':
|
|
cmd.debug = sys.executable.endswith('_d.exe')
|
|
elif sysconfig.get_config_var('Py_ENABLE_SHARED'):
|
|
# To further add to the shared builds fun on Unix, we can't just add
|
|
# library_dirs to the Extension() instance because that doesn't get
|
|
# plumbed through to the final compiler command.
|
|
runshared = sysconfig.get_config_var('RUNSHARED')
|
|
if runshared is None:
|
|
cmd.library_dirs = ['.']
|
|
else:
|
|
if sys.platform == 'darwin':
|
|
cmd.library_dirs = []
|
|
else:
|
|
name, equals, value = runshared.partition('=')
|
|
cmd.library_dirs = value.split(os.pathsep)
|
|
|
|
# Allow tests to run with an uninstalled Python
|
|
if sysconfig.is_python_build():
|
|
pysrcdir = sysconfig.get_config_var('projectbase')
|
|
cmd.distribution.include_dirs.append(os.path.join(pysrcdir, 'Include'))
|
|
|
|
|
|
try:
|
|
from test.support import skip_unless_symlink
|
|
except ImportError:
|
|
skip_unless_symlink = unittest.skip(
|
|
'requires test.support.skip_unless_symlink')
|
|
|
|
skip_2to3_optimize = unittest.skipIf(sys.flags.optimize,
|
|
"2to3 doesn't work under -O")
|