Merge lp:~jtv/maas-test/extract-fixtures into lp:maas-test

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: 121
Merged at revision: 119
Proposed branch: lp:~jtv/maas-test/extract-fixtures
Merge into: lp:maas-test
Diff against target: 474 lines (+235/-100)
5 files modified
maastest/cases.py (+11/-38)
maastest/main.py (+106/-57)
maastest/script.py (+31/-0)
maastest/tests/test_main.py (+85/-3)
setup.py (+2/-2)
To merge this branch: bzr merge lp:~jtv/maas-test/extract-fixtures
Reviewer Review Type Date Requested Status
Gavin Panella (community) Needs Fixing
Graham Binns (community) Approve
Review via email: mp+201185@code.launchpad.net

Commit message

Move proxy/kvm fixtures from test-case class to main().

Description of the change

This is needed in order to be able to perform a second check for unexpected DHCP servers inside the virtual machine. As long as the virtual machine is a fixture in the test case, we can only perform that check inside the test case, where a failure gets misinterpreted as a test failure. When I tried that with a DHCP server running on my network, deliberately causing it to fail, the code even tried to report the fact to Launchpad!

The proxy comes along with the KVMFixture, because the latter needs to know the former. Now, only the MAAS fixture is left inside the test case. That makes a bit more sense, I suppose — a failure to set up the MAAS might be worth reporting. But the purpose of this branch is not to clean up in general, but to pave the way for the extra DHCP check. It will be very easy with these changes in place.

The import style in main.py started getting in the way of unit-testing: it gets awkward to patch() items that are only imported inside a function. But as far as I can see, by far the greatest benefit of late imports was to maintain responsiveness when the user calls maas-test with --help, or makes some basic mistake in usage. I resolved that by creating a new, dedicated little module for the entry point. That's where most of the late imports now happen; once you're past command-line parsing it's OK to do a bit more work.

Jeroen

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

You're a brilliant — if slightly disturbing — man and this is good stuff.

It does have conflicts, but then so does great drama. Fix those and all is happiness and ponies.

review: Approve
lp:~jtv/maas-test/extract-fixtures updated
121. By Jeroen T. Vermeulen

Merge trunk, resolve conflict.

Revision history for this message
Gavin Panella (allenap) wrote :

A few small things that I noticed in passing. The first and last ought
to be fixed, but you were just moving the code so they can just go on
the bug list.

[1]

+        else:
+            # The default case: start polipo and use that as our caching
+            # proxy.
+            proxy_fixture = LocalProxyFixture()
+            proxy_url = proxy_fixture.get_url()
+
+        if proxy_url != '':
+            os.putenv('http_proxy', proxy_url)
+            os.putenv('https_proxy', proxy_url)
+    except:
+        if proxy_fixture is not None:
+            # We have a proxy fixture set up, but we're not going to return.
+            # That means nobody else is responsible for cleaning it up.
+            proxy_fixture.cleanUp()

LocalProxyFixture.setUp() is not called here, so there won't be a
running fixture or much chance of an exception. I assume it's called
elsewhere?

[2]

Regarding [1], it seems strange to put the try...except around the whole
condition. How about:

    if args.http_proxy is not None:
        ...
    else:
        # The default case: start polipo and use that as our caching
        # proxy.
        proxy_fixture = LocalProxyFixture()
        try:
            proxy_fixture.setUp()
        except:
            proxy_fixture.cleanUp()
            raise
        else:
            proxy_url = proxy_fixture.get_url()

    if proxy_url != '':
        os.putenv('http_proxy', proxy_url)
        os.putenv('https_proxy', proxy_url)

?

Otoh, if LocalProxyFixture.setUp() is called elsewhere then there's no
need for the try...except at all.

[3]

+        elif args.disable_cache:
+            # The user has passed --disable-cache, so don't start polipo
+            # and don't try to use an external proxy.
+            proxy_url = ''
+            logging.info("Caching disabled.")
...
+        if proxy_url != '':
+            os.putenv('http_proxy', proxy_url)
+            os.putenv('https_proxy', proxy_url)

If http_proxy is set in the calling environment, but --disable-cache
passed to maas-test, this will leave the calling environment's
http_proxy setting unaltered, which is slightly suprising I think.

How about:

        if proxy_url == '':
            os.unsetenv('http_proxy')
            os.unsetenv('https_proxy')
        else:
            os.putenv('http_proxy', proxy_url)
            os.putenv('https_proxy', proxy_url)

?

review: Needs Fixing

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'maastest/cases.py'
2--- maastest/cases.py 2014-01-10 08:43:44 +0000
3+++ maastest/cases.py 2014-01-10 15:28:21 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2013 Canonical Ltd. This software is licensed under the
6+# Copyright 2013-2014 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """Test whether or not a node is compatible with MAAS."""
10@@ -16,16 +16,12 @@
11
12 import httplib
13 import json
14-import logging
15-import os
16 import sys
17
18 from fixtures import Fixture
19 from maastest import utils
20-from maastest.kvmfixture import KVMFixture
21 from maastest.maas_enums import NODE_STATUS
22 from maastest.maasfixture import MAASFixture
23-from maastest.proxyfixture import LocalProxyFixture
24 import testresources
25 import testtools
26 from testtools.testcase import gather_details
27@@ -54,8 +50,16 @@
28 """Base class for testing machine compatibility with MAAS."""
29
30 @classmethod
31- def configure(cls, args):
32+ def configure(cls, args, machine, proxy_url):
33+ """Set test parameters.
34+
35+ These are class variables, but they are set only on ad-hoc subclasses,
36+ not on `TestMAAS` itself. So this class stays in its original state.
37+ """
38+ assert cls != TestMAAS, "TestMAAS.configure called on TestMAAS."
39 cls.args = args
40+ cls.machine = machine
41+ cls.proxy_url = proxy_url
42
43 fixtures = ClassFixture()
44
45@@ -63,39 +67,8 @@
46 def setUpClass(cls):
47 super(TestMAAS, cls).setUpClass()
48
49- # TODO: series and architecture should be script parameters.
50- architecture = utils.determine_vm_architecture()
51-
52- if cls.args.http_proxy is not None:
53- # The user has passed an external proxy; don't start polipo,
54- # just use that proxy for everything.
55- proxy_url = cls.args.http_proxy
56- logging.info("Using external proxy %s." % proxy_url)
57- os.environ['http_proxy'] = cls.args.http_proxy
58- os.environ['https_proxy'] = cls.args.https_proxy
59- elif cls.args.disable_cache:
60- # The user has passed --disable-cache, so don't start polipo
61- # and don't try to use an external proxy.
62- proxy_url = ''
63- logging.info("Caching disabled.")
64- else:
65- # The default case: start polipo and use that as our caching
66- # proxy.
67- proxy = LocalProxyFixture()
68- cls.fixtures.useFixture(proxy)
69- proxy_url = proxy.get_url()
70- os.environ['http_proxy'] = proxy_url
71- os.environ['https_proxy'] = proxy_url
72-
73- cls.machine = KVMFixture(
74- series=cls.args.maas_series, architecture=architecture,
75- proxy_url=proxy_url, direct_interface=cls.args.interface,
76- archives=cls.args.archive)
77- cls.fixtures.addCleanup(delattr, cls, "machine")
78- cls.fixtures.useFixture(cls.machine)
79-
80 cls.maas = MAASFixture(
81- cls.machine, proxy_url=proxy_url, series=cls.args.series,
82+ cls.machine, proxy_url=cls.proxy_url, series=cls.args.series,
83 architecture=cls.args.architecture)
84 cls.fixtures.addCleanup(delattr, cls, "maas")
85 cls.fixtures.useFixture(cls.maas)
86
87=== modified file 'maastest/main.py'
88--- maastest/main.py 2014-01-10 13:42:34 +0000
89+++ maastest/main.py 2014-01-10 15:28:21 +0000
90@@ -14,10 +14,12 @@
91 "main",
92 ]
93
94+import logging
95+import os
96 import sys
97
98-# Most imports are done as needed in main() to improve responsiveness
99-# when maas-test is run interactively, especially when --help is passed.
100+from maastest import utils
101+from maastest.proxyfixture import LocalProxyFixture
102
103
104 class RETURN_CODES:
105@@ -52,8 +54,6 @@
106
107 :raise ProgramFailure: if the program is not running as root.
108 """
109- # Importing locally for responsiveness.
110- import os
111 if os.geteuid() != 0:
112 raise ProgramFailure(
113 RETURN_CODES.NOT_ROOT, "This test must be run as root. Use sudo.")
114@@ -65,8 +65,6 @@
115 :raise ProgramFailure: if the CPU lacks virtualisation support, or support
116 has been disabled in firmware.
117 """
118- # Importing locally for responsiveness.
119- from maastest import utils
120 if not utils.check_kvm_ok():
121 raise ProgramFailure(
122 RETURN_CODES.NO_KVM_EXTENSIONS,
123@@ -79,9 +77,6 @@
124 For acceptable performance, maas-test should be run on a physical system.
125 This function warns, but does not fail, if the system is virtual.
126 """
127- # Importing locally for responsiveness.
128- import logging
129- from maastest import utils
130 virt_type = utils.virtualization_type()
131 if virt_type is not None:
132 logging.info(
133@@ -111,13 +106,68 @@
134 % (interface, ', '.join(sorted(dhcp_servers))))
135
136
137-def main(argv=None):
138- from maastest.parser import prepare_parser
139- args = prepare_parser().parse_args(argv)
140-
141+def set_up_proxy(args):
142+ """Obtain or set up an http/https proxy, as appropriate.
143+
144+ The command-line arguments may disable this, or make it use an existing
145+ proxy, or require us to set up our own. This function will do whichever
146+ fits, and set up the process environment as needed.
147+
148+ :return: A tuple of (proxy URL, proxy fixture). The fixture may be `None`.
149+ If it is not `None`, it will need to be cleaned up later. The proxy
150+ URL may be the empty string if no proxy is to be used.
151+ """
152+ proxy_fixture = None
153+ try:
154+ if args.http_proxy is not None:
155+ # The user has passed an external proxy; don't start polipo,
156+ # just use that proxy for everything.
157+ proxy_url = args.http_proxy
158+ logging.info("Using external proxy %s." % proxy_url)
159+ elif args.disable_cache:
160+ # The user has passed --disable-cache, so don't start polipo
161+ # and don't try to use an external proxy.
162+ proxy_url = ''
163+ logging.info("Caching disabled.")
164+ else:
165+ # The default case: start polipo and use that as our caching
166+ # proxy.
167+ proxy_fixture = LocalProxyFixture()
168+ proxy_url = proxy_fixture.get_url()
169+
170+ if proxy_url != '':
171+ os.putenv('http_proxy', proxy_url)
172+ os.putenv('https_proxy', proxy_url)
173+ except:
174+ if proxy_fixture is not None:
175+ # We have a proxy fixture set up, but we're not going to return.
176+ # That means nobody else is responsible for cleaning it up.
177+ proxy_fixture.cleanUp()
178+ raise
179+
180+ return proxy_url, proxy_fixture
181+
182+
183+def create_vm(args, proxy_url=None):
184+ """Create the virtual machine.
185+
186+ :param args: Arguments object as returned by the arguments parser.
187+ :param proxy_url: Optional http/https proxy URL for the VM to use, as
188+ returned by `set_up_proxy`.
189+ :return: Virtual-machine fixture. Clean it up after use.
190+ """
191 # Importing locally for responsiveness.
192- from maastest import utils
193- import logging
194+ from maastest.kvmfixture import KVMFixture
195+
196+ # TODO: series and architecture should be script parameters.
197+ architecture = utils.determine_vm_architecture()
198+ return KVMFixture(
199+ series=args.maas_series, architecture=architecture,
200+ proxy_url=proxy_url, direct_interface=args.interface,
201+ archives=args.archive)
202+
203+
204+def main(args):
205 # We tie everything onto one output stream so that we can capture
206 # things for reporting.
207 output_stream = utils.CachingOutputStream(sys.stdout)
208@@ -125,53 +175,52 @@
209 level=logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s',
210 stream=output_stream)
211
212+ proxy_fixture = None
213+ machine_fixture = None
214 try:
215 check_for_root()
216 check_for_virtualisation_support()
217 check_against_virtual_host()
218 check_against_dhcp_servers(args.interface)
219+
220+ proxy_url, proxy_fixture = set_up_proxy(args)
221+ machine_fixture = create_vm(args, proxy_url)
222+
223+ from maastest.cases import TestOneNode
224+
225+ class ConfiguredTestMAAS(TestOneNode):
226+ """A configured version of TestInteractiveOneNode.
227+
228+ ConfiguredTestMAAS is a TestMAAS use to configure it by calling
229+ cls.configure. We need to configure the class itself and not
230+ the instance because the KVMFixture TestMAAS instanciates and needs
231+ to configure is created at the class level.
232+ """
233+ ConfiguredTestMAAS.configure(
234+ args, machine=machine_fixture, proxy_url=proxy_url)
235+
236+ from maastest.console import run_console
237+
238+ result = run_console(ConfiguredTestMAAS, output_stream)
239+
240+ from maastest import report
241+ encoded_results = (
242+ output_stream.cache.getvalue().encode('utf-8'))
243+ if not args.no_reporting:
244+ report.write_test_results(encoded_results)
245+
246+ if not args.no_reporting and not args.log_results_only:
247+ report.report_test_results(encoded_results, result.wasSuccessful())
248+ if result.wasSuccessful():
249+ return RETURN_CODES.SUCCESS
250+ else:
251+ return RETURN_CODES.TEST_FAILED
252+
253 except ProgramFailure as e:
254 logging.error(e)
255 return e.return_code
256-
257- from maastest.cases import TestOneNode
258-
259- class ConfiguredTestMAAS(TestOneNode):
260- """A configured version of TestInteractiveOneNode.
261-
262- ConfiguredTestMAAS is a TestMAAS use to configure it by calling
263- cls.configure. We need to configure the class itself and not
264- the instance because the KVMFixture TestMAAS instanciates and needs
265- to configure is created at the class level.
266- """
267- ConfiguredTestMAAS.configure(args)
268-
269- from maastest.console import run_console
270- # This try/except is a band-aid to make sure the fixtures gets cleaned
271- # up when a KeyboardInterrupt exception is raised.
272- # This does miss out on test-level and module-level tearDowns.
273- try:
274- result = run_console(ConfiguredTestMAAS, output_stream)
275- except KeyboardInterrupt:
276- ConfiguredTestMAAS.tearDownClass()
277- raise
278- from maastest import report
279-
280- encoded_results = (
281- output_stream.cache.getvalue().encode('utf-8'))
282- if not args.no_reporting:
283- report.write_test_results(encoded_results)
284-
285- if not args.no_reporting and not args.log_results_only:
286- report.report_test_results(
287- encoded_results, result.wasSuccessful())
288- if result.wasSuccessful():
289- return RETURN_CODES.SUCCESS
290- else:
291- return RETURN_CODES.TEST_FAILED
292-
293-
294-if __name__ == '__main__':
295- returncode = main()
296- if returncode is not None:
297- sys.exit(returncode)
298+ finally:
299+ if proxy_fixture is not None:
300+ proxy_fixture.cleanUp()
301+ if machine_fixture is not None:
302+ machine_fixture.cleanUp()
303
304=== added file 'maastest/script.py'
305--- maastest/script.py 1970-01-01 00:00:00 +0000
306+++ maastest/script.py 2014-01-10 15:28:21 +0000
307@@ -0,0 +1,31 @@
308+# Copyright 2014 Canonical Ltd. This software is licensed under the
309+# GNU Affero General Public License version 3 (see the file LICENSE).
310+
311+"""Main script entry point for maas-test."""
312+
313+from __future__ import (
314+ absolute_import,
315+ print_function,
316+ unicode_literals,
317+ )
318+
319+str = None
320+
321+__metaclass__ = type
322+__all__ = []
323+
324+import sys
325+
326+from maastest.parser import prepare_parser
327+
328+
329+def entry_point():
330+ args = prepare_parser().parse_args()
331+
332+ # If the user passed --help, we've already exited. This is where we can
333+ # import the rest of the code without hurting responsiveness for that case.
334+ from maastest.main import main
335+ return_code = main(args)
336+
337+ if return_code is not None:
338+ sys.exit(return_code)
339
340=== modified file 'maastest/tests/test_main.py'
341--- maastest/tests/test_main.py 2013-12-17 07:46:12 +0000
342+++ maastest/tests/test_main.py 2014-01-10 15:28:21 +0000
343@@ -1,4 +1,4 @@
344-# Copyright 2013 Canonical Ltd. This software is licensed under the
345+# Copyright 2013-2014 Canonical Ltd. This software is licensed under the
346 # GNU Affero General Public License version 3 (see the file LICENSE).
347
348 """Tests for `maastest.main`."""
349@@ -32,6 +32,13 @@
350
351 class TestTestMAAS(testtools.TestCase):
352
353+ def make_args(self, interface, interactive=True):
354+ """Create a fake arguments object."""
355+ args = mock.MagicMock()
356+ args.interface = interface
357+ args.interactive = interactive
358+ return args
359+
360 def patch_logging(self):
361 """Shut up logging for the duration of this test."""
362 # Quick and dirty. Replace with something nicer if appropriate.
363@@ -69,7 +76,7 @@
364 self.patch(
365 detect_dhcp, 'probe_dhcp', mock.MagicMock(return_value={server}))
366
367- return_value = main.main([interface, '--interactive'])
368+ return_value = main.main(self.make_args(interface))
369
370 self.assertEqual(
371 main.RETURN_CODES.UNEXPECTED_DHCP_SERVER, return_value)
372@@ -80,6 +87,81 @@
373 self.patch_virtualization_type()
374 interface = make_interface_name()
375
376- return_value = main.main([interface, '--interactive'])
377+ return_value = main.main(self.make_args(interface))
378
379 self.assertEqual(main.RETURN_CODES.NOT_ROOT, return_value)
380+
381+
382+def make_proxy_url():
383+ return 'http://%d.example.com:%d' % (randint(1, 999999), randint(1, 65535))
384+
385+
386+class TestSetUpProxy(testtools.TestCase):
387+
388+ def setUp(self):
389+ super(TestSetUpProxy, self).setUp()
390+ self.patch(main, 'LocalProxyFixture', mock.MagicMock())
391+ self.patch(os, 'putenv', mock.MagicMock())
392+
393+ def make_args(self, http_proxy=None, disable_cache=False):
394+ """Create a fake arguments object with the given parameters."""
395+ args = mock.MagicMock()
396+ args.http_proxy = http_proxy
397+ args.disable_cache = disable_cache
398+ return args
399+
400+ def test_returns_empty_if_caching_disabled(self):
401+ self.assertEqual(
402+ ('', None),
403+ main.set_up_proxy(self.make_args(disable_cache=True)))
404+
405+ def test_returns_existing_proxy_if_set(self):
406+ proxy = make_proxy_url()
407+ self.assertEqual(
408+ (proxy, None),
409+ main.set_up_proxy(self.make_args(http_proxy=proxy)))
410+
411+ def test_creates_proxy_if_appropriate(self):
412+ proxy_url, proxy_fixture = main.set_up_proxy(self.make_args())
413+ self.assertIsNotNone(proxy_fixture)
414+ self.addCleanup(proxy_fixture.cleanUp)
415+ self.assertEqual(proxy_fixture.get_url(), proxy_url)
416+
417+ def test_cleans_up_proxy_on_failure(self):
418+ class DeliberateFailure(Exception):
419+ """Deliberately induced error for testing."""
420+
421+ self.patch(
422+ main.LocalProxyFixture.return_value, 'get_url',
423+ mock.MagicMock(side_effect=DeliberateFailure))
424+
425+ self.assertRaises(
426+ DeliberateFailure,
427+ main.set_up_proxy, self.make_args())
428+
429+ self.assertEqual(
430+ [mock.call()],
431+ main.LocalProxyFixture.return_value.cleanUp.mock_calls)
432+
433+ def test_sets_env_to_existing_proxy(self):
434+ proxy = make_proxy_url()
435+ main.set_up_proxy(self.make_args(http_proxy=proxy))
436+ self.assertItemsEqual(
437+ [
438+ mock.call('http_proxy', proxy),
439+ mock.call('https_proxy', proxy),
440+ ],
441+ os.putenv.mock_calls)
442+
443+ def test_sets_env_to_created_proxy(self):
444+ proxy_url, _ = main.set_up_proxy(self.make_args())
445+ self.assertItemsEqual(
446+ [
447+ mock.call('http_proxy', proxy_url),
448+ mock.call('https_proxy', proxy_url),
449+ ],
450+ os.putenv.mock_calls)
451+
452+ def test_leaves_env_unchanged_if_caching_disabled(self):
453+ main.set_up_proxy(self.make_args(disable_cache=True))
454+ self.assertEqual([], os.putenv.mock_calls)
455
456=== modified file 'setup.py'
457--- setup.py 2013-11-13 18:14:48 +0000
458+++ setup.py 2014-01-10 15:28:21 +0000
459@@ -1,5 +1,5 @@
460 #!/usr/bin/env python
461-# Copyright 2013 Canonical Ltd. This software is licensed under the
462+# Copyright 2013-2014 Canonical Ltd. This software is licensed under the
463 # GNU Affero General Public License version 3 (see the file LICENSE).
464
465 """Distutils installer for maas-test."""
466@@ -34,7 +34,7 @@
467 "compatible with MAAS."),
468 entry_points={
469 "console_scripts": [
470- "maas-test = maastest.main:main",
471+ "maas-test = maastest.script:entry_point",
472 ],
473 },
474 )

Subscribers

People subscribed via source and target branches