1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
|
#!/usr/bin/env python
# -*- Mode: python -*-
# Ubuntu CI Engine
# Copyright 2014 Canonical Ltd.
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License version 3, as
# published by the Free Software Foundation.
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
# PURPOSE. See the GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""The CI Engine test runner."""
import logging
import os
import subunit
import sys
from ucitests import (
filters,
loaders,
matchers,
runners,
results,
)
class SysPath(object):
"""Add paths in front of sys.path temporarily."""
def __init__(self, paths):
self.paths = paths
self.orig = sys.path[:]
def __enter__(self):
sys.path = self.paths + sys.path
def __exit__(self, *args):
sys.path = self.orig
def load_component_tests(loader, component_base_dir):
"""Load tests for a given component.
A component is organized inside a given base directory containing files
*and* a python directory with the code and the tests. We care about that
python directory only.
"""
# We set the current directory to the component base one and restore it
# after loading. This could produce duplicate tests if different components
# define the same python modules but this would be a bug in itself so we
# just ignore the potential issue.
suite = loader.suiteClass()
with SysPath([component_base_dir]):
sub_loader = loader.SubLoader(root=component_base_dir)
suite.addTests(sub_loader.loadTestsFromTree('.'))
return suite
def load_regular_component_tests(include_regexps, exclude_regexps=None):
"""Load tests matching inclusive and exclusive regexps.
:param include_regexps: A list of regexps describing the tests to include.
:param exclude_regexps: A list of regexps describing the tests to exclude.
:return: The test suite for all collected tests.
"""
components = ['ci-utils',
'cli',
'branch-source-builder',
'image-builder',
'lander',
'test_runner',
]
loader = loaders.Loader()
# setuptools tends to leave eggs all over the place
loader.dir_matcher = matchers.NameMatcher(includes=[r'.*'],
excludes=[r'\.egg$',
r'\.egg-info$',
])
suite = loader.suiteClass()
for c in components:
suite.addTests(load_component_tests(loader, c))
suite = filters.include_regexps(include_regexps, suite)
suite = filters.exclude_regexps(exclude_regexps, suite)
return suite
def get_runner():
"""Return a test runner class tailored to django needs.
The django design forces us to delay the imports so we can prepare the
environment before django attempt to import modules and set some globals
there.
Since the imports needs to be delayed, that DjangoTestRunner class has to
be defined inside a function so it can inherit from DjangoTestSuiteRunner.
"""
from django.test import (
simple,
utils,
)
class DjangoTestRunner(simple.DjangoTestSuiteRunner):
def __init__(self, component_base_dir, requires_south, out, result,
options,
**kwargs):
super(DjangoTestRunner, self).__init__(**kwargs)
self.component_base_dir = component_base_dir
self.requires_south = requires_south
self.out = out
self.result = result
self.options = options
# Used to cache the test suite
self._cached_test_suite = None
def build_suite(self, *args, **kwargs):
# We cache the test suite to ensure it's loaded only once. This is
# used to workaround DjangoTestSuiteRunner closed design that
# doesn't allow to hook that more precisely. Concretely,
# DjangoTestSuiteRunner.run_tests() setup the databases even if
# there are no tests to run, we take care of that in our own
# run_tests() by not upcalling when the test suite is empty.
if self._cached_test_suite is not None:
return self._cached_test_suite
loader = loaders.Loader()
suite = load_component_tests(loader, self.component_base_dir)
# Filtered as required by the user
suite = filters.include_regexps(
self.options.include_regexps, suite)
suite = filters.exclude_regexps(
self.options.exclude_regexps, suite)
self._cached_test_suite = suite
return suite
def run_tests(self, test_labels, extra_tests=None, **kwargs):
# We load the tests early to avoid the costly database setup when
# it's not needed.
suite = self.build_suite(test_labels, extra_tests, **kwargs)
if self.options.list_only:
ret = runners.list_tests(suite, self.out)
return ret
if suite.countTestCases() == 0:
# No tests to run, we let the caller handle the case where no
# tests are run at all which is an error. By loading the test
# suite early we avoid calling
# DjangoTestSuiteRunner.run_tests() which will setup the
# databases even if the test suite is empty.
return 0
# South needs to patch the way the db is created/synced.
if self.requires_south:
from south.management import commands
commands.patch_for_test_db_setup()
# Otherwise let the base class handle the setup and the run
return super(DjangoTestRunner, self).run_tests(
test_labels, extra_tests=None, **kwargs)
def run_suite(self, suite, **kwargs):
suite.run(self.result)
return self.result
return DjangoTestRunner
class IsolatedImportAndLogging(object):
"""Protect sys.modules, sys.path and logging.root
This allows running django tests which relies on a lot of global context by
taking a snapshot of 'sys.path', 'sys.modules' and 'logging.root' when
entering the context and restoring them afterwards. As such, it creates a
context where django can freely setup globals in modules all over the place
without interfering with the outside.
Several django apps can therefore be tested during the same test run
without stepping on each other toes.
"""
def __init__(self):
# Keep copies of the right objects to be able to restore them
self.paths = list(sys.path)
self.modules = sys.modules.copy()
self.root = logging.root
def __enter__(self):
logging.root.handlers = []
logging.root.setLevel(logging.WARNING)
def __exit__(self, *args):
# Restore the copied list
sys.path = self.paths
# Remove all added modules
added = [m for m in sys.modules.keys() if m not in self.modules]
if added:
for m in added:
del sys.modules[m]
# Restore deleted or modified modules
sys.modules.update(self.modules)
logging.root = self.root
def run_django_tests(component_base_dir, component_name, requires_south,
out, result, options):
"""Run tests for a django app.
:param component_base_dir: Where the component files are.
:param component_name: The name of the component acting as a base for its
python name space.
:param requires_south: Whether the component uses the 'south' module and
needs to be setup accordingly.
:param out: The output stream to use.
:param result: The test result object to use for the run.
:param options: A dict with the options from the command line.
:return: 0 on success, 1 on failure.
"""
with IsolatedImportAndLogging():
# We don't have to use setdefault here because we don't have (and don't
# want !) to support overriding. We want to test with the default
# values defined in the project settings.py
settings_path = '{}.settings'.format(component_name)
os.environ["DJANGO_SETTINGS_MODULE"] = settings_path
kls = get_runner()
test_runner = kls(component_base_dir, requires_south,
out, result, options)
failures = test_runner.run_tests([])
return failures
def run_regular_components(suite, out, result, options):
"""Run tests for all the regular components.
:param suite: The test suite to run.
:param out: The output stream to use.
:param result: The test result object to use for the run.
:param options: A dict with the options from the command line.
:return: 0 on success, 1 on failure.
"""
with IsolatedImportAndLogging():
if options.list_only:
# List the tests without running them
ret = runners.list_tests(suite, out)
else:
suite.run(result)
# TODO: This logic is worth putting into the test result class
# itself or at least a helper should be provided by uci-tests
# itself. -- vila 2014-02-08
ret = int(not (result.wasSuccessful()
and result.testsRun > 0))
return ret
def main(args, stdout, stderr):
# Interpret user wishes
parser = runners.RunTestsArgParser()
options = parser.parse_args(args)
if options.list_only:
# We won't run tests, we won't need a test result
result = None
else:
# Run the tests with the required output
if options.format == 'text':
result = results.TextResult(stdout, verbosity=2)
else:
result = subunit.TestProtocolClient(stdout)
# We'll run tests, start the run
result.startTestRun()
# Load the tests, keeping only the ones required by the user
suite = load_regular_component_tests(options.include_regexps,
options.exclude_regexps)
# Regular components
ret = run_regular_components(suite, stdout, result, options)
# Django-based components
with IsolatedImportAndLogging():
ppa = run_django_tests('ppa-assigner', 'ppa_assigner', True,
stdout, result, options)
ret = ppa or ret
with IsolatedImportAndLogging():
ts = run_django_tests('ticket_system', 'ticket_system', True,
stdout, result, options)
ret = ts or ret
if not options.list_only:
# We did run tests, stop the run
result.stopTestRun()
# Fails if at least one fail
return int(ret)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:], sys.stdout, sys.stderr))
|