Merge lp:~vila/canonical-identity-provider/runner-upstreamed into lp:canonical-identity-provider/release

Proposed by Vincent Ladeuil
Status: Merged
Approved by: Vincent Ladeuil
Approved revision: no longer in the source branch.
Merged at revision: 1390
Proposed branch: lp:~vila/canonical-identity-provider/runner-upstreamed
Merge into: lp:canonical-identity-provider/release
Prerequisite: lp:~vila/canonical-identity-provider/lint
Diff against target: 341 lines (+35/-252)
3 files modified
config-manager.txt (+1/-0)
requirements_devel.txt (+1/-1)
src/testing/runner.py (+33/-251)
To merge this branch: bzr merge lp:~vila/canonical-identity-provider/runner-upstreamed
Reviewer Review Type Date Requested Status
Natalia Bidart (community) Approve
Review via email: mp+280895@code.launchpad.net

Commit message

Remove duplication in test runner concurrency support.

Description of the change

Cleaning after implement concurrent tests, I tried to align with sca, this third step is remove duplication with sca.

This required some refactoring all over the place but the end result is that each project shouldn't have to include boilerplate anymore but focus on the specifics:

- how to isolate the test suite itself (which is where most of the settings common to all tests go),

- whatever alternate runners are defined as long as they provide a command to stream back test results

The ugly duck is bzr+ssh://bazaar.launchpad.net/~vila/+junk/ols-tests-django which doesn't fit uci-tests (no django) nor u1-test-utils (wrong django now that 1.9 is out) but is compatible with django 1.6 (sca) and django 1.7 (sso) which make it fun to test ;)

This requires a new uci-tests: https://code.launchpad.net/~vila/canonical-identity-provider/dependencies/+merge/280891

To post a comment you must log in.
Revision history for this message
Natalia Bidart (nataliabidart) wrote :

As long as revno 10 of ols-tests-django works with 1.8, this looks great!

review: Approve
Revision history for this message
Vincent Ladeuil (vila) wrote :

Yes it does ;)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'config-manager.txt'
2--- config-manager.txt 2015-12-09 11:56:41 +0000
3+++ config-manager.txt 2016-01-07 14:41:41 +0000
4@@ -5,6 +5,7 @@
5 ./branches/django-pgtools bzr+ssh://bazaar.launchpad.net/~canonical-isd-hackers/django-pgtools/trunk;revno=8
6 ./branches/django-piston bzr+ssh://bazaar.launchpad.net/~ubuntuone-pqm-team/django-piston/stable;revno=6
7 ./branches/django-saml2-idp bzr+ssh://bazaar.launchpad.net/~ubuntuone-pqm-team/django-saml2-idp/stable;revno=68
8+./branches/ols-tests-django bzr+ssh://bazaar.launchpad.net/~vila/+junk/ols-tests-django;revno=10
9 ./branches/python-openid bzr+ssh://bazaar.launchpad.net/~ubuntuone-pqm-team/python-openid/stable;revno=1989
10 ./branches/ssoclient bzr+ssh://bazaar.launchpad.net/~canonical-isd-hackers/canonical-identity-provider/ssoclient;revno=7
11
12
13=== added symlink 'lib/olstestsdjango'
14=== target is u'../branches/ols-tests-django/olstestsdjango'
15=== modified file 'requirements_devel.txt'
16--- requirements_devel.txt 2015-12-21 22:51:15 +0000
17+++ requirements_devel.txt 2016-01-07 14:41:41 +0000
18@@ -26,6 +26,6 @@
19 testscenarios==0.4
20 testtools==0.9.39
21 u1-test-utils==0.5
22-ucitests==0.1.8
23+ucitests==0.2.1
24 wsgi-intercept==0.5.1
25 -r requirements_docs.txt
26
27=== modified file 'src/testing/runner.py'
28--- src/testing/runner.py 2015-11-26 09:10:03 +0000
29+++ src/testing/runner.py 2016-01-07 14:41:41 +0000
30@@ -1,4 +1,3 @@
31-import optparse
32 import os
33 import shutil
34 import subprocess
35@@ -9,14 +8,8 @@
36 from django.utils import unittest
37 from django.test import runner as dj_runner
38 from gargoyle import gargoyle
39-import subunit
40-import testtools
41-from ucitests import (
42- filters,
43- results,
44- runners,
45- suites,
46-)
47+from olstestsdjango import runner
48+from ucitests import suites
49
50 try:
51 import cProfile as profile
52@@ -68,169 +61,6 @@
53 connection.creation.destroy_test_db(old_name, verbosity=1)
54
55
56-class TestRunner(dj_runner.DiscoverRunner):
57-
58- option_list = dj_runner.DiscoverRunner.option_list + (
59- optparse.make_option(
60- '-c', '--concurrency',
61- action='store', dest='concurrency', type='int',
62- default=1,
63- help='How many concurrent processes to run the tests.'),
64- optparse.make_option(
65- '-l', '--list',
66- action='store_true', dest='list_only',
67- default=False,
68- help='List test ids without running them.'),
69- optparse.make_option(
70- '-i', '--include',
71- action='store', dest='include',
72- default=None,
73- help='Tests matching this regexp will be run.'),
74- optparse.make_option(
75- '-x', '--exclude',
76- action='store', dest='exclude',
77- default=None,
78- help='Tests matching this regexp will not be run.'),
79- optparse.make_option(
80- '--load',
81- action='store', dest='load_file',
82- default=None,
83- help='A file name containing test ids to be run.'),
84- optparse.make_option(
85- '-f', '--format',
86- action='store', dest='format', type='choice',
87- choices=['text', 'subunit'], default='text',
88- help='Output format for the test results.'),
89- optparse.make_option(
90- '-s', '--sub-runner',
91- action='store', dest='sub_runner', type='choice',
92- choices=['fork', 'lxc'], default='fork',
93- help='The concurrent test runner to use.'),
94- )
95-
96- def __init__(self, pattern=None, top_level=None, verbosity=1,
97- interactive=True, failfast=False, concurrency=1,
98- list_only=False, include=None, exclude=None,
99- load_file=None, format=None,
100- sub_runner=None,
101- output=None,
102- **kwargs):
103- super(TestRunner, self).__init__(
104- pattern, top_level, verbosity=verbosity, interactive=interactive,
105- failfast=failfast, **kwargs)
106- self.list_only = list_only
107- if output is None:
108- output = sys.stdout
109- self.output = output
110- if include is not None:
111- self.includes = include.split(':')
112- else:
113- self.includes = []
114- if exclude is not None:
115- self.excludes = exclude.split(':')
116- else:
117- self.excludes = []
118- self.concurrency = concurrency
119- self.load_file = load_file
120- self.format = format
121- self.sub_runner = sub_runner
122-
123- def setup_databases(self, **kwargs):
124- """Override base class to defer the db setup.
125-
126- The db setup id done in SSOTestSuite so each worker can use its own
127- db.
128-
129- """
130- # Return an empty but valid value so teardown_databases does nothing
131- # when invoked from the base class run_suite().
132- return ([], [])
133-
134- def build_suite(self, test_labels=None, extra_tests=None, **kwargs):
135- if not test_labels:
136- # Without this, only the tests in the acceptance folder
137- # will be tried to be run when no labels are specified.
138- test_labels = ('src',)
139- suite = super(TestRunner, self).build_suite(
140- test_labels, extra_tests, **kwargs)
141- suite = filters.include_regexps(self.includes, suite)
142- suite = filters.exclude_regexps(self.excludes, suite)
143- if self.load_file is not None:
144- # Load the file and filter the suite
145- with open(self.load_file, 'r') as f:
146- id_list = filters.TestIdList(filters.load_test_id_list(f))
147- suite = filters.include_id_list(id_list, suite)
148- wrapping_suite = SSOTestSuite(suite)
149- return wrapping_suite
150-
151- def run_tests(self, test_labels, extra_tests=None, **kwargs):
152- # Override base class so we can list the tests without the costly
153- # setup.
154- if self.list_only:
155- suite = self.build_suite(test_labels, extra_tests)
156- for t in testtools.testsuite.iterate_tests(suite):
157- self.output.write(t.id() + '\n')
158- return 0
159- # Otherwise let the base class handle the setup and the run
160- return super(TestRunner, self).run_tests(test_labels, extra_tests=None,
161- **kwargs)
162-
163- def run_suite(self, suite, **kwargs):
164- initial_test_nb = suite.countTestCases()
165- if self.concurrency > 1:
166- if self.sub_runner == 'fork':
167- concurrent_runner = runners.run_suite_forked
168- elif self.sub_runner == 'lxc':
169- concurrent_runner = run_suite_in_container
170- concurrent_suite = testtools.ConcurrentTestSuite(
171- suite, runners.split_suite_for(self.concurrency,
172- concurrent_runner))
173- else:
174- concurrent_suite = suite
175- if self.format == 'text':
176- result = results.TextResult(sys.stdout,
177- verbosity=self.verbosity,
178- failfast=self.failfast)
179- else:
180- result = subunit.TestProtocolClient(sys.stdout)
181- result.startTestRun()
182- try:
183- concurrent_suite.run(result)
184- self.check_all_tests_executed(initial_test_nb, result)
185- except subprocess.CalledProcessError as e:
186- sys.stderr.write('{}'.format(e))
187- raise
188- finally:
189- result.stopTestRun()
190- return result
191-
192- def check_all_tests_executed(self, initial_test_nb, result):
193- # In very rare failure modes, tests that should be run may not
194- # increment 'testsRun'. This sanity check will catch them.
195- try:
196- if initial_test_nb != result.testsRun:
197- raise AssertionError(
198- 'Expecting {}, got {} tests run'.format(
199- initial_test_nb, result.testsRun))
200- except:
201- # Defined here to get a revealing class path without risking test
202- # discovery to pick us.
203- class RunSuiteIntegrity(unittest.TestCase):
204- # 'run' is just an existing method, we won't execute that test
205- runTest = unittest.TestCase.run
206-
207- t = RunSuiteIntegrity('run')
208- result.errors.append((t, result._exc_info_to_string(sys.exc_info(),
209- t)))
210- # The above avoid a spurious 'ERROR' on the output stream if we use
211- # the following (clearer and not relying on private details) call
212- # result.addError(RunSuiteIntegrity('run'), sys.exc_info())
213-
214-
215-# FIXME: More of the following should be upstreamed to uci-{tests,vms}. Part of
216-# it has been stolen from there to start with. -- vila 2015-06-09
217-
218-
219 def subprocess_run(args):
220 try:
221 return subprocess.check_output(args,
222@@ -244,88 +74,40 @@
223 raise
224
225
226-class TestInContainer(object):
227-
228- def __init__(self, worker_name, test_list_path, worker_num):
229- super(TestInContainer, self).__init__()
230- self.worker_name = worker_name
231- self.test_list_path = test_list_path
232- self.worker_num = worker_num
233-
234- def __call__(self, result=None):
235- # comply with unittests.TestCase API
236- return self.run(result)
237-
238- def run(self, result):
239+class SSOContainerSuite(suites.SubprocessedSuite):
240+
241+ def setUp(self):
242+ super(SSOContainerSuite, self).setUp()
243+ # FIXME: We use a file in the current directory as a cheap way to share
244+ # it with the container which bind mount the user HOME. It would be
245+ # better to upload it to the container instead but it's more
246+ # involved. In the mean time, for debugging isolation issues, it may
247+ # help to start with the 'self.test_list_path' file (taking a copy
248+ # here) -- vila 2015-12-08
249 here = os.getcwd()
250 # Spawn the ephemeral container
251+ self.worker_name = 'sso-worker-{:>02d}'.format(self.unique)
252 subprocess_run(['uci-vms', 'start', self.worker_name])
253- try:
254- # Finish container setup (food for uci-vms there)
255- subprocess_run(['uci-vms', 'shell', self.worker_name,
256- '(', 'cd', here, '&&',
257- 'make', 'start-db',
258- ')'])
259- # Run the tests, connecting the pipes
260- cmd = ['uci-vms', 'shell', self.worker_name,
261- # And now we run the tests inside the container
262- '(', 'cd', here, '&&',
263- 'make', 'test',
264- 'ARGS="-f subunit --load {}"'.format(self.test_list_path),
265- ')']
266- process = subprocess.Popen(cmd,
267- stdin=subprocess.PIPE,
268- stdout=subprocess.PIPE,
269- # line buffering for subunit
270- bufsize=1)
271- # run the subunit test that collect the real tests results
272- test = TestInSubprocess(process)
273- test.run(result)
274- finally:
275- # Whatever happens, cleanup.
276- # Note that for debugging isolation issues, it may help to start
277- # with the 'self.test_list_path' file, so commenting the following
278- # line will preserve all lists.
279- os.remove(self.test_list_path)
280- # And tear down the ephemeral container
281- subprocess_run(['uci-vms', 'stop', self.worker_name])
282-
283-
284-class TestInSubprocess(subunit.ProtocolTestCase):
285-
286- def __init__(self, process):
287- super(TestInSubprocess, self).__init__(process.stdout)
288- self.process = process
289- self.process.stdin.close()
290-
291- def run(self, result):
292- try:
293- super(TestInSubprocess, self).run(result)
294- finally:
295- self.process.wait()
296-
297-
298-def run_suite_in_container(unique, child_suite, parent_suite):
299- """Execute a test suite into a container.
300-
301- :param unique: A number uniquely identifying the suite/container.
302-
303- :param child_suite: A TestSuite object to be run in the container.
304-
305- :param parent_suite: A TestSuite object where the parent test collecting
306- the child results should be added.
307- """
308- worker_name = 'sso-worker-{:>02d}'.format(unique)
309- # FIXME: We use a file in the current directory as a cheap way to share it
310- # with the container which bind mount the user HOME. It would be better to
311- # upload it to the container instead but it's more involved. -- vila
312- # 2015-06-09
313- test_list_path = '{}.test-list'.format(worker_name)
314- with open(test_list_path, 'w') as tl:
315- for t in filters.iter_flat(child_suite):
316- tl.write('{}\n'.format(t.id()))
317- test = TestInContainer(worker_name, test_list_path, unique)
318- parent_suite.addTest(test)
319+ self.addCleanup(subprocess_run, ['uci-vms', 'stop', self.worker_name])
320+ # Finish container setup (food for a make target?)
321+ subprocess_run(['uci-vms', 'shell', self.worker_name,
322+ '(', 'cd', here, '&&',
323+ 'make', 'start-db',
324+ ')'])
325+ # Build the command running the tests, connecting the pipes
326+ self.cmd = ['uci-vms', 'shell', self.worker_name,
327+ # And now we run the tests inside the container
328+ '(', 'cd', here, '&&',
329+ 'make', 'test',
330+ 'ARGS="-f subunit --load {}"'.format(self.test_list_path),
331+ ')']
332+
333+
334+class TestRunner(runner.TestRunner):
335+
336+ split_suites = dict(fork=suites.ForkedSuite,
337+ lxc=SSOContainerSuite)
338+ wrapping_test_suite = SSOTestSuite
339
340
341 class ProfilingTestSuiteRunner(TestRunner):