Merge lp:~frankban/charms/precise/juju-gui/testing-improvements into lp:~juju-gui/charms/precise/juju-gui/trunk

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 68
Proposed branch: lp:~frankban/charms/precise/juju-gui/testing-improvements
Merge into: lp:~juju-gui/charms/precise/juju-gui/trunk
Diff against target: 440 lines (+155/-58)
10 files modified
.lbox.check (+3/-0)
HACKING.md (+0/-5)
Makefile (+1/-1)
revision (+1/-1)
tests/20-functional.test (+6/-6)
tests/deploy.py (+10/-8)
tests/helpers.py (+33/-9)
tests/requirements.pip (+2/-0)
tests/test_deploy.py (+30/-13)
tests/test_helpers.py (+69/-15)
To merge this branch: bzr merge lp:~frankban/charms/precise/juju-gui/testing-improvements
Reviewer Review Type Date Requested Status
charmers Pending
Review via email: mp+168673@code.launchpad.net

Description of the change

Charm test improvements.

This branch includes some improvements to
the charm testing infrastructure:

Strengthened the Juju implementation check by
introducing a new juju_version helper function.

Do not require the branch to be named "juju-gui"
anymore: the charm can be tested and deployed from
an arbitrary branch.

Updated documentation.

Added the .lbox.check file: now, before
proposing/submitting at least the lint check
and the unit tests must pass.

https://codereview.appspot.com/9714047/

To post a comment you must log in.
Revision history for this message
Francesco Banconi (frankban) wrote :

Reviewers: mp+168673_code.launchpad.net,

Message:
Please take a look.

Description:
Charm test improvements.

This branch includes some improvements to
the charm testing infrastructure:

Strengthened the Juju implementation check by
introducing a new juju_version helper function.

Do not require the branch to be named "juju-gui"
anymore: the charm can be tested and deployed from
an arbitrary branch.

Updated documentation.

Added the .lbox.check file: now, before
proposing/submitting at least the lint check
and the unit tests must pass.

https://code.launchpad.net/~frankban/charms/precise/juju-gui/testing-improvements/+merge/168673

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/9714047/

Affected files:
   A .lbox.check
   M HACKING.md
   A [revision details]
   M tests/20-functional.test
   M tests/deploy.py
   M tests/helpers.py
   M tests/requirements.pip
   M tests/test_deploy.py
   M tests/test_helpers.py

76. By Francesco Banconi

Bump revision up.

Revision history for this message
Francesco Banconi (frankban) wrote :
Revision history for this message
Nicola Larosa (teknico) wrote :

LGTM, thanks for this. Nice tests as usual. A few trivials below.

https://codereview.appspot.com/9714047/diff/3001/tests/20-functional.test
File tests/20-functional.test (right):

https://codereview.appspot.com/9714047/diff/3001/tests/20-functional.test#newcode74
tests/20-functional.test:74: def wait_for(self, condition, error=None,
timeout=30):
Maybe a constant at top of file for all three default timeout values?

https://codereview.appspot.com/9714047/diff/3001/tests/helpers.py
File tests/helpers.py (right):

https://codereview.appspot.com/9714047/diff/3001/tests/helpers.py#newcode137
tests/helpers.py:137: except subprocess.CalledProcessError:
This means it's goJuju, right? A comment to that effect?

https://codereview.appspot.com/9714047/diff/3001/tests/helpers.py#newcode142
tests/helpers.py:142: toint = lambda num: 0 if num is None else int(num)
This looks like a mispelling of "taint". :-) "to_int" maybe?

https://codereview.appspot.com/9714047/diff/3001/tests/requirements.pip
File tests/requirements.pip (right):

https://codereview.appspot.com/9714047/diff/3001/tests/requirements.pip#newcode1
tests/requirements.pip:1: # Juju GUI test requirements.
Oh, you can put comments in here? Good to know. :-)

https://codereview.appspot.com/9714047/diff/3001/tests/test_helpers.py
File tests/test_helpers.py (right):

https://codereview.appspot.com/9714047/diff/3001/tests/test_helpers.py#newcode209
tests/test_helpers.py:209: def test_not_semantic(self,
mock_check_output):
Maybe a more explicit "test_not_semantic_versioning"?

https://codereview.appspot.com/9714047/

77. By Francesco Banconi

Fix make lint.

Revision history for this message
Francesco Banconi (frankban) wrote :
Revision history for this message
j.c.sackett (jcsackett) wrote :

On 2013/06/11 14:58:05, frankban wrote:
> Please take a look.

LGTM.

I have nothing to add, though I do agree wtih teknico's suggestion of
using a constant for the default timeout value in 20-functional.test

https://codereview.appspot.com/9714047/

78. By Francesco Banconi

Changes as per review.

79. By Francesco Banconi

Merged trunk and resolved conflicts.

Revision history for this message
Francesco Banconi (frankban) wrote :

*** Submitted:

Charm test improvements.

This branch includes some improvements to
the charm testing infrastructure:

Strengthened the Juju implementation check by
introducing a new juju_version helper function.

Do not require the branch to be named "juju-gui"
anymore: the charm can be tested and deployed from
an arbitrary branch.

Updated documentation.

Added the .lbox.check file: now, before
proposing/submitting at least the lint check
and the unit tests must pass.

R=teknico, j.c.sackett
CC=
https://codereview.appspot.com/9714047

https://codereview.appspot.com/9714047/diff/3001/tests/20-functional.test
File tests/20-functional.test (right):

https://codereview.appspot.com/9714047/diff/3001/tests/20-functional.test#newcode74
tests/20-functional.test:74: def wait_for(self, condition, error=None,
timeout=30):
On 2013/06/11 14:38:40, teknico wrote:
> Maybe a constant at top of file for all three default timeout values?

As discussed on IRC, those values are only incidentally the same, so
making a module level constant doesn't give us a lot.

https://codereview.appspot.com/9714047/diff/3001/tests/helpers.py
File tests/helpers.py (right):

https://codereview.appspot.com/9714047/diff/3001/tests/helpers.py#newcode137
tests/helpers.py:137: except subprocess.CalledProcessError:
On 2013/06/11 14:38:40, teknico wrote:
> This means it's goJuju, right? A comment to that effect?

Done.

https://codereview.appspot.com/9714047/diff/3001/tests/helpers.py#newcode142
tests/helpers.py:142: toint = lambda num: 0 if num is None else int(num)
On 2013/06/11 14:38:40, teknico wrote:
> This looks like a mispelling of "taint". :-) "to_int" maybe?

Done.

https://codereview.appspot.com/9714047/diff/3001/tests/test_helpers.py
File tests/test_helpers.py (right):

https://codereview.appspot.com/9714047/diff/3001/tests/test_helpers.py#newcode209
tests/test_helpers.py:209: def test_not_semantic(self,
mock_check_output):
On 2013/06/11 14:38:40, teknico wrote:
> Maybe a more explicit "test_not_semantic_versioning"?

Done.

https://codereview.appspot.com/9714047/

Revision history for this message
Francesco Banconi (frankban) wrote :

Thank you both for the reviews!

https://codereview.appspot.com/9714047/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file '.lbox.check'
2--- .lbox.check 1970-01-01 00:00:00 +0000
3+++ .lbox.check 2013-06-12 09:11:27 +0000
4@@ -0,0 +1,3 @@
5+#!/bin/sh
6+
7+make lint unittest
8
9=== modified file 'HACKING.md'
10--- HACKING.md 2013-06-11 14:04:04 +0000
11+++ HACKING.md 2013-06-12 09:11:27 +0000
12@@ -43,11 +43,6 @@
13 At this point, link "juju-plugins/plugins/juju_test.py" as "juju-test"
14 somewhere in your PATH, so that it is possible to execute "juju-test".
15
16-Our testing infrastructure requires the current directory name to match the
17-charm name, so you must check out the charm into a directory named "juju-gui":
18-
19- bzr branch lp:~juju-gui/charms/precise/juju-gui/trunk juju-gui
20-
21 Before being able to run the suite, test requirements need to be installed
22 running the command:
23
24
25=== modified file 'Makefile'
26--- Makefile 2013-06-11 14:13:45 +0000
27+++ Makefile 2013-06-12 09:11:27 +0000
28@@ -36,7 +36,7 @@
29 $(JUJUTEST)
30
31 lint: setup
32- @flake8 --show-source --exclude=.venv ./hooks/ ./tests/
33+ @$(VENV)/bin/flake8 --show-source --exclude=.venv ./hooks/ ./tests/
34
35 clean:
36 find . -name '*.pyc' -delete
37
38=== modified file 'revision'
39--- revision 2013-06-12 08:59:15 +0000
40+++ revision 2013-06-12 09:11:27 +0000
41@@ -1,1 +1,1 @@
42-52
43+53
44
45=== modified file 'tests/20-functional.test'
46--- tests/20-functional.test 2013-06-11 14:04:04 +0000
47+++ tests/20-functional.test 2013-06-12 09:11:27 +0000
48@@ -26,14 +26,14 @@
49 from deploy import juju_deploy
50 from helpers import (
51 juju_destroy_service,
52- legacy_juju,
53+ juju_version,
54 ssh,
55 )
56
57
58 JUJU_GUI_TEST_BRANCH = 'lp:~juju-gui/juju-gui/charm-tests-branch'
59 STAGING_SERVICES = ('haproxy', 'mediawiki', 'memcached', 'mysql', 'wordpress')
60-is_legacy_juju = legacy_juju()
61+is_legacy_juju = juju_version().major == 0
62
63
64 class DeployTestMixin(object):
65@@ -56,7 +56,7 @@
66 def assertEnvironmentIsConnected(self):
67 """Assert the GUI environment is connected to the Juju API agent."""
68 self.wait_for_script(
69- 'return app.env.get("connected");',
70+ 'return app && app.env && app.env.get("connected");',
71 error='Environment not connected.')
72
73 def handle_browser_warning(self):
74@@ -87,7 +87,7 @@
75 return driver.title == 'Juju Admin'
76 self.wait_for(page_ready, error='Juju GUI not found.')
77
78- def wait_for(self, condition, error=None, timeout=20):
79+ def wait_for(self, condition, error=None, timeout=30):
80 """Wait for condition to be True.
81
82 The argument condition is a callable accepting a driver object.
83@@ -97,7 +97,7 @@
84 wait = ui.WebDriverWait(self.selenium, timeout)
85 return wait.until(condition, error)
86
87- def wait_for_css_selector(self, selector, error=None, timeout=20):
88+ def wait_for_css_selector(self, selector, error=None, timeout=30):
89 """Wait until the provided CSS selector is found.
90
91 Fail printing the provided error if timeout is exceeded.
92@@ -108,7 +108,7 @@
93 elements = self.wait_for(condition, error=error, timeout=timeout)
94 return elements[0]
95
96- def wait_for_script(self, script, error=None, timeout=20):
97+ def wait_for_script(self, script, error=None, timeout=30):
98 """Wait for the given JavaScript snippet to return a True value.
99
100 Fail printing the provided error if timeout is exceeded.
101
102=== modified file 'tests/deploy.py'
103--- tests/deploy.py 2013-06-11 14:04:04 +0000
104+++ tests/deploy.py 2013-06-12 09:11:27 +0000
105@@ -33,23 +33,25 @@
106 rsync = command('rsync', '-a', '--exclude', '.bzr', '--exclude', '.venv')
107
108
109-def setup_repository(source, series='precise'):
110+def setup_repository(name, source, series='precise'):
111 """Create a temporary Juju repository to use for charm deployment.
112
113- Copy the charm files in source in the precise repository section, excluding
114- the virtualenv and Bazaar directories.
115+ Copy the charm files in source in the precise repository section, using the
116+ provided charm name and excluding the virtualenv and Bazaar directories.
117
118 Return the repository path.
119 """
120- source = os.path.abspath(source)
121+ source = os.path.abspath(source) + os.path.sep
122 repo = tempfile.mkdtemp()
123- destination = os.path.join(repo, series)
124+ destination = os.path.join(repo, series, name)
125 os.makedirs(destination)
126 rsync(source, destination)
127 return repo
128
129
130-def juju_deploy(charm, options=None, force_machine=None, charm_source=None):
131+def juju_deploy(
132+ charm, options=None, force_machine=None, charm_source=None,
133+ series='precise'):
134 """Deploy and expose the charm. Return the first unit's public address.
135
136 Also wait until the service is exposed and the first unit started.
137@@ -60,14 +62,14 @@
138 if charm_source is None:
139 # Dynamically retrieve the charm source based on the path of this file.
140 charm_source = os.path.join(os.path.dirname(__file__), '..')
141- repo = setup_repository(charm_source)
142+ repo = setup_repository(charm, charm_source, series=series)
143 args = ['deploy', '--repository', repo]
144 if options is not None:
145 config_file = make_charm_config_file({charm: options})
146 args.extend(['--config', config_file.name])
147 if force_machine is not None:
148 args.extend(['--force-machine', str(force_machine)])
149- args.append('local:{0}'.format(charm))
150+ args.append('local:{}/{}'.format(series, charm))
151 juju(*args)
152 juju('expose', charm)
153 return wait_for_unit(charm)
154
155=== modified file 'tests/helpers.py'
156--- tests/helpers.py 2013-06-11 14:04:04 +0000
157+++ tests/helpers.py 2013-06-12 09:11:27 +0000
158@@ -16,9 +16,11 @@
159
160 """Juju GUI test helpers."""
161
162+from collections import namedtuple
163 from functools import wraps
164 import json
165 import os
166+import re
167 import subprocess
168 import time
169
170@@ -71,15 +73,7 @@
171 juju_command = command('juju')
172 juju_env = lambda: os.getenv('JUJU_ENV') # This is propagated by juju-test.
173 ssh = command('ssh')
174-
175-
176-def legacy_juju():
177- """Return True if pyJuju is being used, False otherwise."""
178- try:
179- juju_command('--version')
180- except ProcessError:
181- return False
182- return True
183+Version = namedtuple('Version', 'major minor patch')
184
185
186 def retry(exception, tries=10, delay=1):
187@@ -136,6 +130,36 @@
188 return json.loads(status)
189
190
191+_juju_version_expression = re.compile(r"""
192+ ^ # Beginning of line.
193+ (?:juju\s+)? # Optional juju prefix.
194+ (\d+)\.(\d+) # Major and minor versions.
195+ (?:\.(\d+))? # Optional patch version.
196+ .* # Optional suffix.
197+ $ # End of line.
198+""", re.VERBOSE)
199+
200+
201+def juju_version():
202+ """Return the currently used Juju version.
203+
204+ The version is returned as a named tuple (major, minor, patch).
205+ If the patch number is missing, it is set to zero.
206+ """
207+ try:
208+ # In pyJuju, version info is printed to stderr.
209+ output = subprocess.check_output(
210+ ['juju', '--version'], stderr=subprocess.STDOUT)
211+ except subprocess.CalledProcessError:
212+ # Current juju-core exposes a version subcommand.
213+ output = subprocess.check_output(['juju', 'version'])
214+ match = _juju_version_expression.match(output)
215+ if match is None:
216+ raise ValueError('invalid juju version: {!r}'.format(output))
217+ to_int = lambda num: 0 if num is None else int(num)
218+ return Version._make(map(to_int, match.groups()))
219+
220+
221 def wait_for_unit(sevice):
222 """Wait for the first unit of the given service to be started.
223
224
225=== modified file 'tests/requirements.pip'
226--- tests/requirements.pip 2013-06-11 14:43:19 +0000
227+++ tests/requirements.pip 2013-06-12 09:11:27 +0000
228@@ -1,3 +1,5 @@
229+# Juju GUI test requirements.
230+
231 # This file is part of the Juju GUI, which lets users view and manage Juju
232 # environments within a graphical interface (https://launchpad.net/juju-gui).
233 # Copyright (C) 2012-2013 Canonical Ltd.
234
235=== modified file 'tests/test_deploy.py'
236--- tests/test_deploy.py 2013-06-11 14:04:04 +0000
237+++ tests/test_deploy.py 2013-06-12 09:11:27 +0000
238@@ -31,10 +31,11 @@
239
240 class TestSetupRepository(unittest.TestCase):
241
242+ name = 'test-charm'
243+
244 def setUp(self):
245 # Create a directory structure for the charm source.
246 self.source = tempfile.mkdtemp()
247- self.charm_name = os.path.basename(self.source)
248 self.addCleanup(shutil.rmtree, self.source)
249 # Create a file in the source dir.
250 _, self.root_file = tempfile.mkstemp(dir=self.source)
251@@ -75,24 +76,24 @@
252 series_dir = os.path.join(repo, series)
253 self.assert_dir_exists(series_dir)
254 # The series directory only contains our charm.
255- self.assertEqual([self.charm_name], os.listdir(series_dir))
256- self.assert_dir_exists(os.path.join(series_dir, self.charm_name))
257+ self.assertEqual([self.name], os.listdir(series_dir))
258+ self.assert_dir_exists(os.path.join(series_dir, self.name))
259
260 def test_repository(self):
261 # The charm repository is correctly created with the default series.
262- repo = setup_repository(self.source)
263+ repo = setup_repository(self.name, self.source)
264 self.check_repository(repo, 'precise')
265
266 def test_series(self):
267 # The charm repository is created with the given series.
268- repo = setup_repository(self.source, series='raring')
269+ repo = setup_repository(self.name, self.source, series='raring')
270 self.check_repository(repo, 'raring')
271
272 def test_charm_files(self):
273 # The charm files are correctly copied inside the repository, excluding
274 # unwanted directories.
275- repo = setup_repository(self.source)
276- charm_dir = os.path.join(repo, 'precise', self.charm_name)
277+ repo = setup_repository(self.name, self.source)
278+ charm_dir = os.path.join(repo, 'precise', self.name)
279 test_dir_name = os.path.basename(self.tests_dir)
280 expected = set([
281 os.path.basename(self.root_file),
282@@ -107,7 +108,7 @@
283 unit_info = {'public-address': 'unit.example.com'}
284 charm = 'test-charm'
285 expose_call = mock.call('expose', charm)
286- local_charm = 'local:{}'.format(charm)
287+ local_charm = 'local:precise/{}'.format(charm)
288 repo = '/tmp/repo/'
289
290 @mock.patch('deploy.juju')
291@@ -115,13 +116,19 @@
292 @mock.patch('deploy.setup_repository')
293 def call_deploy(
294 self, mock_setup_repository, mock_wait_for_unit, mock_juju,
295- **kwargs):
296+ options=None, force_machine=None, charm_source=None,
297+ series='precise'):
298 mock_setup_repository.return_value = self.repo
299 mock_wait_for_unit.return_value = self.unit_info
300- charm_source = kwargs.setdefault(
301- 'charm_source', os.path.join(os.path.dirname(__file__), '..'))
302- unit_info = juju_deploy(self.charm, **kwargs)
303- mock_setup_repository.assert_called_once_with(charm_source)
304+ if charm_source is None:
305+ expected_source = os.path.join(os.path.dirname(__file__), '..')
306+ else:
307+ expected_source = charm_source
308+ unit_info = juju_deploy(
309+ self.charm, options=options, force_machine=force_machine,
310+ charm_source=charm_source, series=series)
311+ mock_setup_repository.assert_called_once_with(
312+ self.charm, expected_source, series=series)
313 # The unit address is correctly returned.
314 self.assertEqual(self.unit_info, unit_info)
315 self.assertEqual(1, mock_wait_for_unit.call_count)
316@@ -177,3 +184,13 @@
317 )
318 deploy_call = self.call_deploy(charm_source='/tmp/source/')
319 self.assertEqual(expected_deploy_call, deploy_call)
320+
321+ def test_series(self):
322+ # The function can deploy a charm from a specific series.
323+ expected_deploy_call = mock.call(
324+ 'deploy',
325+ '--repository', self.repo,
326+ 'local:raring/{}'.format(self.charm)
327+ )
328+ deploy_call = self.call_deploy(series='raring')
329+ self.assertEqual(expected_deploy_call, deploy_call)
330
331=== modified file 'tests/test_helpers.py'
332--- tests/test_helpers.py 2013-06-11 14:04:04 +0000
333+++ tests/test_helpers.py 2013-06-12 09:11:27 +0000
334@@ -17,6 +17,7 @@
335 """Juju GUI helpers tests."""
336
337 import json
338+import subprocess
339 import unittest
340
341 import mock
342@@ -27,9 +28,10 @@
343 juju_destroy_service,
344 juju_env,
345 juju_status,
346- legacy_juju,
347+ juju_version,
348 ProcessError,
349 retry,
350+ Version,
351 wait_for_unit,
352 )
353
354@@ -193,20 +195,72 @@
355 mock_juju.assert_called_once_with('status', '--format', 'json')
356
357
358-@mock.patch('helpers.juju_command')
359-class TestLegacyJuju(unittest.TestCase):
360-
361- def test_pyjuju(self, mock_juju_command):
362- # Legacy Juju is correctly recognized.
363- mock_juju_command.return_value = '0.7.0'
364- self.assertTrue(legacy_juju())
365- mock_juju_command.assert_called_once_with('--version')
366-
367- def test_juju_core(self, mock_juju_command):
368- # juju-core is correctly recognized.
369- mock_juju_command.side_effect = ProcessError(1, 'failed', '', '')
370- self.assertFalse(legacy_juju())
371- mock_juju_command.assert_called_once_with('--version')
372+@mock.patch('subprocess.check_output')
373+class TestJujuVersion(unittest.TestCase):
374+
375+ error = subprocess.CalledProcessError(2, 'invalid flag', 'output')
376+
377+ def test_pyjuju(self, mock_check_output):
378+ # The pyJuju version is correctly retrieved.
379+ mock_check_output.return_value = '0.7.2'
380+ version = juju_version()
381+ self.assertEqual(Version(0, 7, 2), version)
382+ mock_check_output.assert_called_once_with(
383+ ['juju', '--version'], stderr=subprocess.STDOUT,
384+ )
385+
386+ def test_juju_core(self, mock_check_output):
387+ # The juju-core version is correctly retrieved.
388+ mock_check_output.side_effect = (self.error, '1.12.3')
389+ version = juju_version()
390+ self.assertEqual(Version(1, 12, 3), version)
391+ self.assertEqual(2, mock_check_output.call_count)
392+ first_call, second_call = mock_check_output.call_args_list
393+ self.assertEqual(
394+ mock.call(['juju', '--version'], stderr=subprocess.STDOUT),
395+ first_call,
396+ )
397+ self.assertEqual(mock.call(['juju', 'version']), second_call)
398+
399+ def test_not_semantic_versioning(self, mock_check_output):
400+ # If the patch number is missing, it is set to zero.
401+ mock_check_output.return_value = '0.7'
402+ version = juju_version()
403+ self.assertEqual(Version(0, 7, 0), version)
404+
405+ def test_prefix(self, mock_check_output):
406+ # The function handles versions returned as "juju x.y.z".
407+ mock_check_output.return_value = 'juju 0.8.3'
408+ version = juju_version()
409+ self.assertEqual(Version(0, 8, 3), version)
410+
411+ def test_suffix(self, mock_check_output):
412+ # The function handles versions returned as "x.y.z-series-arch".
413+ mock_check_output.return_value = '1.10.3-raring-amd64'
414+ version = juju_version()
415+ self.assertEqual(Version(1, 10, 3), version)
416+
417+ def test_all(self, mock_check_output):
418+ # Additional information is correctly handled.
419+ mock_check_output.side_effect = (self.error, 'juju 1.234-precise-i386')
420+ version = juju_version()
421+ self.assertEqual(Version(1, 234, 0), version)
422+ self.assertEqual(2, mock_check_output.call_count)
423+
424+ def test_invalid_version(self, mock_check_output):
425+ # A ValueError is raised if the returned version is not valid.
426+ mock_check_output.return_value = '42'
427+ with self.assertRaises(ValueError) as info:
428+ juju_version()
429+ self.assertEqual("invalid juju version: '42'", str(info.exception))
430+
431+ def test_failure(self, mock_check_output):
432+ # A CalledProcessError is raised if the Juju version cannot be found.
433+ mock_check_output.side_effect = (self.error, self.error)
434+ with self.assertRaises(subprocess.CalledProcessError) as info:
435+ juju_version()
436+ self.assertIs(self.error, info.exception)
437+ self.assertEqual(2, mock_check_output.call_count)
438
439
440 class TestProcessError(unittest.TestCase):

Subscribers

People subscribed via source and target branches