1"""Support code for distutils test cases."""
2import os
3import sys
4import shutil
5import tempfile
6import unittest
7import sysconfig
8from copy import deepcopy
9
10from distutils import log
11from distutils.log import DEBUG, INFO, WARN, ERROR, FATAL
12from distutils.core import Distribution
13
14
15class LoggingSilencer(object):
16
17    def setUp(self):
18        super().setUp()
19        self.threshold = log.set_threshold(log.FATAL)
20        # catching warnings
21        # when log will be replaced by logging
22        # we won't need such monkey-patch anymore
23        self._old_log = log.Log._log
24        log.Log._log = self._log
25        self.logs = []
26
27    def tearDown(self):
28        log.set_threshold(self.threshold)
29        log.Log._log = self._old_log
30        super().tearDown()
31
32    def _log(self, level, msg, args):
33        if level not in (DEBUG, INFO, WARN, ERROR, FATAL):
34            raise ValueError('%s wrong log level' % str(level))
35        if not isinstance(msg, str):
36            raise TypeError("msg should be str, not '%.200s'"
37                            % (type(msg).__name__))
38        self.logs.append((level, msg, args))
39
40    def get_logs(self, *levels):
41        def _format(msg, args):
42            return msg % args
43        return [msg % args for level, msg, args
44                in self.logs if level in levels]
45
46    def clear_logs(self):
47        self.logs = []
48
49
50class TempdirManager(object):
51    """Mix-in class that handles temporary directories for test cases.
52
53    This is intended to be used with unittest.TestCase.
54    """
55
56    def setUp(self):
57        super().setUp()
58        self.old_cwd = os.getcwd()
59        self.tempdirs = []
60
61    def tearDown(self):
62        # Restore working dir, for Solaris and derivatives, where rmdir()
63        # on the current directory fails.
64        os.chdir(self.old_cwd)
65        super().tearDown()
66        while self.tempdirs:
67            d = self.tempdirs.pop()
68            shutil.rmtree(d, os.name in ('nt', 'cygwin'))
69
70    def mkdtemp(self):
71        """Create a temporary directory that will be cleaned up.
72
73        Returns the path of the directory.
74        """
75        d = tempfile.mkdtemp()
76        self.tempdirs.append(d)
77        return d
78
79    def write_file(self, path, content='xxx'):
80        """Writes a file in the given path.
81
82
83        path can be a string or a sequence.
84        """
85        if isinstance(path, (list, tuple)):
86            path = os.path.join(*path)
87        f = open(path, 'w')
88        try:
89            f.write(content)
90        finally:
91            f.close()
92
93    def create_dist(self, pkg_name='foo', **kw):
94        """Will generate a test environment.
95
96        This function creates:
97         - a Distribution instance using keywords
98         - a temporary directory with a package structure
99
100        It returns the package directory and the distribution
101        instance.
102        """
103        tmp_dir = self.mkdtemp()
104        pkg_dir = os.path.join(tmp_dir, pkg_name)
105        os.mkdir(pkg_dir)
106        dist = Distribution(attrs=kw)
107
108        return pkg_dir, dist
109
110
111class DummyCommand:
112    """Class to store options for retrieval via set_undefined_options()."""
113
114    def __init__(self, **kwargs):
115        for kw, val in kwargs.items():
116            setattr(self, kw, val)
117
118    def ensure_finalized(self):
119        pass
120
121
122class EnvironGuard(object):
123
124    def setUp(self):
125        super(EnvironGuard, self).setUp()
126        self.old_environ = deepcopy(os.environ)
127
128    def tearDown(self):
129        for key, value in self.old_environ.items():
130            if os.environ.get(key) != value:
131                os.environ[key] = value
132
133        for key in tuple(os.environ.keys()):
134            if key not in self.old_environ:
135                del os.environ[key]
136
137        super(EnvironGuard, self).tearDown()
138
139
140def copy_xxmodule_c(directory):
141    """Helper for tests that need the xxmodule.c source file.
142
143    Example use:
144
145        def test_compile(self):
146            copy_xxmodule_c(self.tmpdir)
147            self.assertIn('xxmodule.c', os.listdir(self.tmpdir))
148
149    If the source file can be found, it will be copied to *directory*.  If not,
150    the test will be skipped.  Errors during copy are not caught.
151    """
152    filename = _get_xxmodule_path()
153    if filename is None:
154        raise unittest.SkipTest('cannot find xxmodule.c (test must run in '
155                                'the python build dir)')
156    shutil.copy(filename, directory)
157
158
159def _get_xxmodule_path():
160    srcdir = sysconfig.get_config_var('srcdir')
161    candidates = [
162        # use installed copy if available
163        os.path.join(os.path.dirname(__file__), 'xxmodule.c'),
164        # otherwise try using copy from build directory
165        os.path.join(srcdir, 'Modules', 'xxmodule.c'),
166        # srcdir mysteriously can be $srcdir/Lib/distutils/tests when
167        # this file is run from its parent directory, so walk up the
168        # tree to find the real srcdir
169        os.path.join(srcdir, '..', '..', '..', 'Modules', 'xxmodule.c'),
170    ]
171    for path in candidates:
172        if os.path.exists(path):
173            return path
174
175
176def fixup_build_ext(cmd):
177    """Function needed to make build_ext tests pass.
178
179    When Python was built with --enable-shared on Unix, -L. is not enough to
180    find libpython<blah>.so, because regrtest runs in a tempdir, not in the
181    source directory where the .so lives.
182
183    When Python was built with in debug mode on Windows, build_ext commands
184    need their debug attribute set, and it is not done automatically for
185    some reason.
186
187    This function handles both of these things.  Example use:
188
189        cmd = build_ext(dist)
190        support.fixup_build_ext(cmd)
191        cmd.ensure_finalized()
192
193    Unlike most other Unix platforms, Mac OS X embeds absolute paths
194    to shared libraries into executables, so the fixup is not needed there.
195    """
196    if os.name == 'nt':
197        cmd.debug = sys.executable.endswith('_d.exe')
198    elif sysconfig.get_config_var('Py_ENABLE_SHARED'):
199        # To further add to the shared builds fun on Unix, we can't just add
200        # library_dirs to the Extension() instance because that doesn't get
201        # plumbed through to the final compiler command.
202        runshared = sysconfig.get_config_var('RUNSHARED')
203        if runshared is None:
204            cmd.library_dirs = ['.']
205        else:
206            if sys.platform == 'darwin':
207                cmd.library_dirs = []
208            else:
209                name, equals, value = runshared.partition('=')
210                cmd.library_dirs = [d for d in value.split(os.pathsep) if d]
211