]>
Commit | Line | Data |
---|---|---|
53e6db90 DC |
1 | import os |
2 | import operator | |
3 | import sys | |
4 | import contextlib | |
5 | import itertools | |
6 | import unittest | |
7 | from distutils.errors import DistutilsError, DistutilsOptionError | |
8 | from distutils import log | |
9 | from unittest import TestLoader | |
10 | ||
11 | from pkg_resources import ( | |
12 | resource_listdir, | |
13 | resource_exists, | |
14 | normalize_path, | |
15 | working_set, | |
16 | evaluate_marker, | |
17 | add_activation_listener, | |
18 | require, | |
19 | ) | |
20 | from .._importlib import metadata | |
21 | from setuptools import Command | |
22 | from setuptools.extern.more_itertools import unique_everseen | |
23 | from setuptools.extern.jaraco.functools import pass_none | |
24 | ||
25 | ||
26 | class ScanningLoader(TestLoader): | |
27 | def __init__(self): | |
28 | TestLoader.__init__(self) | |
29 | self._visited = set() | |
30 | ||
31 | def loadTestsFromModule(self, module, pattern=None): | |
32 | """Return a suite of all tests cases contained in the given module | |
33 | ||
34 | If the module is a package, load tests from all the modules in it. | |
35 | If the module has an ``additional_tests`` function, call it and add | |
36 | the return value to the tests. | |
37 | """ | |
38 | if module in self._visited: | |
39 | return None | |
40 | self._visited.add(module) | |
41 | ||
42 | tests = [] | |
43 | tests.append(TestLoader.loadTestsFromModule(self, module)) | |
44 | ||
45 | if hasattr(module, "additional_tests"): | |
46 | tests.append(module.additional_tests()) | |
47 | ||
48 | if hasattr(module, '__path__'): | |
49 | for file in resource_listdir(module.__name__, ''): | |
50 | if file.endswith('.py') and file != '__init__.py': | |
51 | submodule = module.__name__ + '.' + file[:-3] | |
52 | else: | |
53 | if resource_exists(module.__name__, file + '/__init__.py'): | |
54 | submodule = module.__name__ + '.' + file | |
55 | else: | |
56 | continue | |
57 | tests.append(self.loadTestsFromName(submodule)) | |
58 | ||
59 | if len(tests) != 1: | |
60 | return self.suiteClass(tests) | |
61 | else: | |
62 | return tests[0] # don't create a nested suite for only one return | |
63 | ||
64 | ||
65 | # adapted from jaraco.classes.properties:NonDataProperty | |
66 | class NonDataProperty: | |
67 | def __init__(self, fget): | |
68 | self.fget = fget | |
69 | ||
70 | def __get__(self, obj, objtype=None): | |
71 | if obj is None: | |
72 | return self | |
73 | return self.fget(obj) | |
74 | ||
75 | ||
76 | class test(Command): | |
77 | """Command to run unit tests after in-place build""" | |
78 | ||
79 | description = "run unit tests after in-place build (deprecated)" | |
80 | ||
81 | user_options = [ | |
82 | ('test-module=', 'm', "Run 'test_suite' in specified module"), | |
83 | ( | |
84 | 'test-suite=', | |
85 | 's', | |
86 | "Run single test, case or suite (e.g. 'module.test_suite')", | |
87 | ), | |
88 | ('test-runner=', 'r', "Test runner to use"), | |
89 | ] | |
90 | ||
91 | def initialize_options(self): | |
92 | self.test_suite = None | |
93 | self.test_module = None | |
94 | self.test_loader = None | |
95 | self.test_runner = None | |
96 | ||
97 | def finalize_options(self): | |
98 | ||
99 | if self.test_suite and self.test_module: | |
100 | msg = "You may specify a module or a suite, but not both" | |
101 | raise DistutilsOptionError(msg) | |
102 | ||
103 | if self.test_suite is None: | |
104 | if self.test_module is None: | |
105 | self.test_suite = self.distribution.test_suite | |
106 | else: | |
107 | self.test_suite = self.test_module + ".test_suite" | |
108 | ||
109 | if self.test_loader is None: | |
110 | self.test_loader = getattr(self.distribution, 'test_loader', None) | |
111 | if self.test_loader is None: | |
112 | self.test_loader = "setuptools.command.test:ScanningLoader" | |
113 | if self.test_runner is None: | |
114 | self.test_runner = getattr(self.distribution, 'test_runner', None) | |
115 | ||
116 | @NonDataProperty | |
117 | def test_args(self): | |
118 | return list(self._test_args()) | |
119 | ||
120 | def _test_args(self): | |
121 | if not self.test_suite: | |
122 | yield 'discover' | |
123 | if self.verbose: | |
124 | yield '--verbose' | |
125 | if self.test_suite: | |
126 | yield self.test_suite | |
127 | ||
128 | def with_project_on_sys_path(self, func): | |
129 | """ | |
130 | Backward compatibility for project_on_sys_path context. | |
131 | """ | |
132 | with self.project_on_sys_path(): | |
133 | func() | |
134 | ||
135 | @contextlib.contextmanager | |
136 | def project_on_sys_path(self, include_dists=[]): | |
137 | self.run_command('egg_info') | |
138 | ||
139 | # Build extensions in-place | |
140 | self.reinitialize_command('build_ext', inplace=1) | |
141 | self.run_command('build_ext') | |
142 | ||
143 | ei_cmd = self.get_finalized_command("egg_info") | |
144 | ||
145 | old_path = sys.path[:] | |
146 | old_modules = sys.modules.copy() | |
147 | ||
148 | try: | |
149 | project_path = normalize_path(ei_cmd.egg_base) | |
150 | sys.path.insert(0, project_path) | |
151 | working_set.__init__() | |
152 | add_activation_listener(lambda dist: dist.activate()) | |
153 | require('%s==%s' % (ei_cmd.egg_name, ei_cmd.egg_version)) | |
154 | with self.paths_on_pythonpath([project_path]): | |
155 | yield | |
156 | finally: | |
157 | sys.path[:] = old_path | |
158 | sys.modules.clear() | |
159 | sys.modules.update(old_modules) | |
160 | working_set.__init__() | |
161 | ||
162 | @staticmethod | |
163 | @contextlib.contextmanager | |
164 | def paths_on_pythonpath(paths): | |
165 | """ | |
166 | Add the indicated paths to the head of the PYTHONPATH environment | |
167 | variable so that subprocesses will also see the packages at | |
168 | these paths. | |
169 | ||
170 | Do this in a context that restores the value on exit. | |
171 | """ | |
172 | nothing = object() | |
173 | orig_pythonpath = os.environ.get('PYTHONPATH', nothing) | |
174 | current_pythonpath = os.environ.get('PYTHONPATH', '') | |
175 | try: | |
176 | prefix = os.pathsep.join(unique_everseen(paths)) | |
177 | to_join = filter(None, [prefix, current_pythonpath]) | |
178 | new_path = os.pathsep.join(to_join) | |
179 | if new_path: | |
180 | os.environ['PYTHONPATH'] = new_path | |
181 | yield | |
182 | finally: | |
183 | if orig_pythonpath is nothing: | |
184 | os.environ.pop('PYTHONPATH', None) | |
185 | else: | |
186 | os.environ['PYTHONPATH'] = orig_pythonpath | |
187 | ||
188 | @staticmethod | |
189 | def install_dists(dist): | |
190 | """ | |
191 | Install the requirements indicated by self.distribution and | |
192 | return an iterable of the dists that were built. | |
193 | """ | |
194 | ir_d = dist.fetch_build_eggs(dist.install_requires) | |
195 | tr_d = dist.fetch_build_eggs(dist.tests_require or []) | |
196 | er_d = dist.fetch_build_eggs( | |
197 | v | |
198 | for k, v in dist.extras_require.items() | |
199 | if k.startswith(':') and evaluate_marker(k[1:]) | |
200 | ) | |
201 | return itertools.chain(ir_d, tr_d, er_d) | |
202 | ||
203 | def run(self): | |
204 | self.announce( | |
205 | "WARNING: Testing via this command is deprecated and will be " | |
206 | "removed in a future version. Users looking for a generic test " | |
207 | "entry point independent of test runner are encouraged to use " | |
208 | "tox.", | |
209 | log.WARN, | |
210 | ) | |
211 | ||
212 | installed_dists = self.install_dists(self.distribution) | |
213 | ||
214 | cmd = ' '.join(self._argv) | |
215 | if self.dry_run: | |
216 | self.announce('skipping "%s" (dry run)' % cmd) | |
217 | return | |
218 | ||
219 | self.announce('running "%s"' % cmd) | |
220 | ||
221 | paths = map(operator.attrgetter('location'), installed_dists) | |
222 | with self.paths_on_pythonpath(paths): | |
223 | with self.project_on_sys_path(): | |
224 | self.run_tests() | |
225 | ||
226 | def run_tests(self): | |
227 | test = unittest.main( | |
228 | None, | |
229 | None, | |
230 | self._argv, | |
231 | testLoader=self._resolve_as_ep(self.test_loader), | |
232 | testRunner=self._resolve_as_ep(self.test_runner), | |
233 | exit=False, | |
234 | ) | |
235 | if not test.result.wasSuccessful(): | |
236 | msg = 'Test failed: %s' % test.result | |
237 | self.announce(msg, log.ERROR) | |
238 | raise DistutilsError(msg) | |
239 | ||
240 | @property | |
241 | def _argv(self): | |
242 | return ['unittest'] + self.test_args | |
243 | ||
244 | @staticmethod | |
245 | @pass_none | |
246 | def _resolve_as_ep(val): | |
247 | """ | |
248 | Load the indicated attribute value, called, as a as if it were | |
249 | specified as an entry point. | |
250 | """ | |
251 | return metadata.EntryPoint(value=val, name=None, group=None).load()() |