Merge lp:~frankban/charms/precise/juju-gui/use-juju-test into lp:~juju-gui/charms/precise/juju-gui/trunk
- Precise Pangolin (12.04)
- use-juju-test
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 65 |
Proposed branch: | lp:~frankban/charms/precise/juju-gui/use-juju-test |
Merge into: | lp:~juju-gui/charms/precise/juju-gui/trunk |
Diff against target: |
1302 lines (+896/-185) 14 files modified
.bzrignore (+1/-0) HACKING.md (+66/-82) Makefile (+49/-0) revision (+1/-1) tests/00-setup (+20/-0) tests/10-unit.test (+5/-3) tests/20-functional.test (+40/-94) tests/deploy.py (+62/-0) tests/helpers.py (+145/-0) tests/requirements.pip (+10/-0) tests/test_backends.py (+4/-2) tests/test_deploy.py (+163/-0) tests/test_helpers.py (+327/-0) tests/test_utils.py (+3/-3) |
To merge this branch: | bzr merge lp:~frankban/charms/precise/juju-gui/use-juju-test |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
charmers | Pending | ||
Review via email: mp+168079@code.launchpad.net |
Commit message
Description of the change
Replace jitsu test with juju-test.
This branch makes juju-test the default test runner
for both the unit tests and the charm functional tests.
The charm test suite now supports both pyJuju and juju-core.
The tests dependencies are now installed in a virtualenv that
is automatically created and updated by the first test
executable (00-setup). This way we don't require the user
to install all the packages, and, moreover, we make the tests
ready to be run in the charm testing infrastructure.
When the charm is deployed during tests, a temporary repository
is created where the charm is copied and then deployed.
This way we achieve two important goals:
- the developer is not forced to manually set up a local
Juju repository where to branch the Juju GUI;
- the charm can be deployed even if it contains a virtualenv, i.e.
a directory with absolute symlinks (that's because the .venv dir
is not copied in the temp repo).
The tests/helpers.py and tests/deploy.py files will be eventually
replaced by similar tools provided by the juju testing harness.
Updated the documentation where required.
Fixed unit tests exit code.
Common commands (test/lint/clean etc.) can now be easily run
using make.
Nice to have, not included in this branch:
- collect deployment test logs;
- reintroduce a way to test the charm specifying a
different juju-gui-source;
- do not require the branch to be named "juju-gui".
PS: sorry for the diff, well over 1k loc :-/
Francesco Banconi (frankban) wrote : | # |
Gary Poster (gary) wrote : | # |
Wow, this looks great. I'll give it a LGTM conditional on at least one
other person doing qa along with a review.
https:/
File HACKING.md (right):
https:/
HACKING.md:19: sudo apt-get install build-essential bzr libapt-pkg-dev
That's better :-)
https:/
HACKING.md:28: time of this writing it is not yet released. To run it
you must first install
lol that this part didn't have to change. Hopefully it won't stay that
way.
https:/
HACKING.md:36: The current incarnation of our testing infrastructure
requires that the current
Suggest simplifying old stuff:
Our testing infrastructure requires the current directory name to match
the charm name, so you...
https:/
HACKING.md:38: directory named "juju-gui":
Agreed that removing this requirement would be nice to have for later.
Seems like it would be pretty easy given the new approach, yeah?
https:/
HACKING.md:42: Before being able to run the suite, tests requirements
need to be installed
s/tests/test/
https:/
HACKING.md:96: If you have set up your environment to run you local
development charm,
s/run you/run your/
https:/
HACKING.md:117: created, the testing virtualenv.
Nice
https:/
File Makefile (right):
https:/
Makefile:1: # Juju GUI Makefile.
I was going to ask you to add a copyright notice, but I see that we need
to do that for the whole charm. I'll make a card.
Nicola Larosa (teknico) wrote : | # |
LGTM, very nice work, a couple trivials. Did not QA yet, will do on
Monday.
https:/
File tests/20-
https:/
tests/20-
Swap the two lines above?
https:/
File tests/helpers.py (right):
https:/
tests/helpers.
s/appeneded/
https:/
tests/helpers.
Nice docstring. :-)
Brad Crittenden (bac) wrote : | # |
LGTM but I cannot get the tests to run cleanly and I have some
documentation suggestions.
Trying to run with pyjuju I get the following failures
http://
I figure it is landable if these failures can be addressed.
https:/
File HACKING.md (right):
https:/
HACKING.md:67: make unittest
Excellent
https:/
HACKING.md:81: In the command above, "myenv" is the juju environment, as
it is specified in
myenv was first introduced on line 60 but not explained until here,
which is confusing.
Also, it is not totally obvious which environment you should pick.
After some investigation I see that it depends on the version of juju in
your path. Since environment.yaml stanzas are different for pyjuju and
juju-core you have to carefully pick an environment that matches the
juju in your path. That should be stated explicitly in this document.
For some makefile targets the environment is optional and the default is
used. Can this not be applied to them all?
https:/
File tests/helpers.py (right):
https:/
tests/helpers.
This check seems very fragile. If juju-core ever grows a '--version'
option this breaks.
- 77. By Francesco Banconi
-
Changes as per review.
- 78. By Francesco Banconi
-
s/lp/http/ in requirements.
Francesco Banconi (frankban) wrote : | # |
Please take a look.
https:/
File HACKING.md (right):
https:/
HACKING.md:19: sudo apt-get install build-essential bzr libapt-pkg-dev
On 2013/06/07 16:33:34, gary.poster wrote:
> That's better :-)
Indeed! However, I forgot two important
dependencies here: pip and virtualenv.
Fixed.
https:/
HACKING.md:36: The current incarnation of our testing infrastructure
requires that the current
On 2013/06/07 16:33:34, gary.poster wrote:
> Suggest simplifying old stuff:
> Our testing infrastructure requires the current directory name to
match the
> charm name, so you...
Done.
https:/
HACKING.md:38: directory named "juju-gui":
On 2013/06/07 16:33:35, gary.poster wrote:
> Agreed that removing this requirement would be nice to have for later.
Seems
> like it would be pretty easy given the new approach, yeah?
Correct, it should not be a problem.
https:/
HACKING.md:42: Before being able to run the suite, tests requirements
need to be installed
On 2013/06/07 16:33:35, gary.poster wrote:
> s/tests/test/
Done.
https:/
HACKING.md:81: In the command above, "myenv" is the juju environment, as
it is specified in
On 2013/06/07 20:05:58, bac wrote:
> myenv was first introduced on line 60 but not explained until here,
which is
> confusing.
Fixed.
> Also, it is not totally obvious which environment you should pick.
After some
> investigation I see that it depends on the version of juju in your
path. Since
> environment.yaml stanzas are different for pyjuju and juju-core you
have to
> carefully pick an environment that matches the juju in your path.
Not necessarily. Or are there problems preventing you to use
the same environment? There are some cases where one env is not
compatible with different juju implementations (e.g. the local provider
is used, or juju-origin is specified). Other than that, AFAIK, it is
absolutely licit to boostrap, for example, the same ec2 environment from
both pyJuju and juju-core. Am I missing something?
> That should
> be stated explicitly in this document.
The documentation already includes a link to the relevant part of
the Juju doc explaining what is and how to set up an environment.yaml.
> For some makefile targets the environment is optional and the default
is used.
> Can this not be applied to them all?
This depends on juju-test explicitly requiring an environment name to
be passed. For this reason the make targets wrapping juju-test have the
same requirement. We could parse the environment.yaml and find ourself
the
name of the default env, but I believe that's out of scope for this
branch,
and maybe also in general for the charm tests. I guess the ability to
use the default env if not specified otherwise could quite easily
be added to juju-test in the future.
https:/
HACKING.md:96: If you have set up your environment to...
Francesco Banconi (frankban) wrote : | # |
On 2013/06/07 20:05:58, bac wrote:
> Trying to run with pyjuju I get the following failures
> http://
This is weird. When you are available, if you agree,
I'd like to investigate this issue with you.
Nicola Larosa (teknico) wrote : | # |
teknico wrote:
> Did not QA yet, will do on Monday.
QA completed successfully, on both pyjuju and juju-core. It took some
more tweaking to the juju-test command, thanks for the collaboration.
- 79. By Francesco Banconi
-
Add xvfb to the list of requirements.
Francesco Banconi (frankban) wrote : | # |
*** Submitted:
Replace jitsu test with juju-test.
This branch makes juju-test the default test runner
for both the unit tests and the charm functional tests.
The charm test suite now supports both pyJuju and juju-core.
The tests dependencies are now installed in a virtualenv that
is automatically created and updated by the first test
executable (00-setup). This way we don't require the user
to install all the packages, and, moreover, we make the tests
ready to be run in the charm testing infrastructure.
When the charm is deployed during tests, a temporary repository
is created where the charm is copied and then deployed.
This way we achieve two important goals:
- the developer is not forced to manually set up a local
Juju repository where to branch the Juju GUI;
- the charm can be deployed even if it contains a virtualenv, i.e.
a directory with absolute symlinks (that's because the .venv dir
is not copied in the temp repo).
The tests/helpers.py and tests/deploy.py files will be eventually
replaced by similar tools provided by the juju testing harness.
Updated the documentation where required.
Fixed unit tests exit code.
Common commands (test/lint/clean etc.) can now be easily run
using make.
Nice to have, not included in this branch:
- collect deployment test logs;
- reintroduce a way to test the charm specifying a
different juju-gui-source;
- do not require the branch to be named "juju-gui".
PS: sorry for the diff, well over 1k loc :-/
Francesco Banconi (frankban) wrote : | # |
Hi Gary, Nicola and Brad,
thanks for reviewing and QAing this branch!
Preview Diff
1 | === modified file '.bzrignore' |
2 | --- .bzrignore 2013-05-02 09:49:51 +0000 |
3 | +++ .bzrignore 2013-06-10 14:12:23 +0000 |
4 | @@ -2,3 +2,4 @@ |
5 | tags |
6 | exec.d/* |
7 | npm-cache.tgz |
8 | +tests/.venv |
9 | |
10 | === modified file 'HACKING.md' |
11 | --- HACKING.md 2013-05-22 13:19:26 +0000 |
12 | +++ HACKING.md 2013-06-10 14:12:23 +0000 |
13 | @@ -16,8 +16,8 @@ |
14 | |
15 | You'll also need some dependencies and developer basics. |
16 | |
17 | - sudo apt-get install bzr autoconf libtool python-charmhelpers python-yaml \ |
18 | - xvfb |
19 | + sudo apt-get install build-essential bzr libapt-pkg-dev python-pip \ |
20 | + python-virtualenv xvfb |
21 | |
22 | Next, you need the bzr branch. We work from |
23 | [lp:~juju-gui/charms/precise/juju-gui/trunk](https://code.launchpad.net/~juju-gui/charms/precise/juju-gui/trunk). |
24 | @@ -25,49 +25,50 @@ |
25 | You could start hacking now, but there's a bit more to do to prepare for |
26 | running and writing tests. |
27 | |
28 | -We use the Jitsu test command to run our functional tests. At the time of |
29 | -this writing it is not yet released. To run it you must first install it |
30 | -locally. The files may be installed globally, or into your home directory (as |
31 | -here): |
32 | - |
33 | - sudo apt-get install autoconf libtool python-charmhelpers python-tempita |
34 | - bzr branch lp:juju-jitsu juju-jitsu |
35 | - cd juju-jitsu |
36 | - autoreconf |
37 | - ./configure --prefix=$HOME |
38 | +We use the juju-test test command to run our functional and unit tests. At the |
39 | +time of this writing it is not yet released. To run it you must first install |
40 | +it locally, e.g.: |
41 | + |
42 | + bzr checkout --lightweight lp:juju-plugins |
43 | + |
44 | +At this point, link "juju-plugins/plugins/juju_test.py" as "juju-test" |
45 | +somewhere in your PATH, so that it is possible to execute "juju-test". |
46 | + |
47 | +Our testing infrastructure requires the current directory name to match the |
48 | +charm name, so you must check out the charm into a directory named "juju-gui": |
49 | + |
50 | + bzr branch lp:~juju-gui/charms/precise/juju-gui/trunk juju-gui |
51 | + |
52 | +Before being able to run the suite, test requirements need to be installed |
53 | +running the command: |
54 | + |
55 | make |
56 | - make install |
57 | - |
58 | -Functional tests make use of Selenium and xvfbwrapper. To install the latest |
59 | -version of these packages, as required by the test suite, you can use pip or |
60 | -easy_install, e.g.: |
61 | - |
62 | - sudo pip install selenium xvfbwrapper |
63 | - |
64 | -The current incarnation of the Jitsu test command requires that the current |
65 | -directory name match the charm name, so you must check out the charm into a |
66 | -directory named "juju-gui": |
67 | - |
68 | - bzr branch lp:~juju-gui/charms/precise/juju-gui/trunk juju-gui |
69 | - |
70 | -The branch directory must be placed (or linked from) within a local charm |
71 | -repository. It consists of a directory, itself containing a number of |
72 | -directories, one for each distribution codename, e.g. `precise`. In turn, the |
73 | -codename directories will contain the charm directories. Therefore, you |
74 | -should put your charm in a path like this: `[REPO]/precise/juju-gui`. |
75 | - |
76 | -Now you are ready to run the functional tests (see the next section). |
77 | + |
78 | +The command above will create a ".venv" directory inside "juju-gui/tests/", |
79 | +ignored by DVCSes, containing the development virtual environment with all the |
80 | +testing dependencies. Run "make help" to see all the available make targets. |
81 | + |
82 | +Now you are ready to run the functional and unit tests (see the next section). |
83 | |
84 | ## Testing ## |
85 | |
86 | There are two types of tests for the charm: unit tests and functional tests. |
87 | +Long story short, to run all the tests: |
88 | + |
89 | + make test JUJU_ENV="myenv" |
90 | + |
91 | +In the command above, "myenv" is the juju environment, as it is specified in |
92 | +your `~/.juju/environments.yaml`, that will be bootstrapped before running the |
93 | +tests and destroyed at the end of the test run. |
94 | + |
95 | +Please read further for additional details. |
96 | |
97 | ### Unit Tests ### |
98 | |
99 | The unit tests do not require a functional Juju environment, and can be run |
100 | with this command:: |
101 | |
102 | - python tests/unit.test |
103 | + make unittest |
104 | |
105 | Unit tests should be created in the "tests" subdirectory and be named in the |
106 | customary way (i.e., "test_*.py"). |
107 | @@ -75,33 +76,15 @@ |
108 | ### Functional Tests ### |
109 | |
110 | Running the functional tests requires a Juju testing environment as provided |
111 | -by the Jitsu test command (see "Getting Started", above). All files in the |
112 | -tests directory which end with ".test" will be run in a Juju Jitsu test |
113 | -environment. |
114 | - |
115 | -Jitsu requires the charm directory to be named the same as the charm and to be |
116 | -the current working directory when the tests are run: |
117 | - |
118 | - JUJU_REPOSITORY=/path/to/charm/repo ~/bin/jitsu test juju-gui \ |
119 | - --logdir /tmp --timeout 40m |
120 | - |
121 | -This command will bootstrap the default Juju environment specified in your |
122 | -`~/.juju/environments.yaml`. |
123 | - |
124 | -Functional tests deploy, by default, the latest stable release of Juju GUI. |
125 | -However, it is possible to provide a different Juju GUI source by setting the |
126 | -environment variable `JUJU_GUI_SOURCE` while running tests, e.g.: |
127 | - |
128 | - JUJU_GUI_SOURCE=lp:mybranch JUJU_REPOSITORY=/path/to/charm/repo \ |
129 | - ~/bin/jitsu test juju-gui ... |
130 | - |
131 | -To test a new trunk release: |
132 | - |
133 | - JUJU_GUI_SOURCE=trunk JUJU_REPOSITORY=/path/to/charm/repo \ |
134 | - ~/bin/jitsu test juju-gui ... |
135 | - |
136 | -This can be useful to verify that a given branch works with the charm, which is |
137 | -one of the tasks that release QA should include. |
138 | +by the juju-test command (see "Getting Started", above). |
139 | + |
140 | +To run only the functional tests: |
141 | + |
142 | + make ftest JUJU_ENV="myenv" |
143 | + |
144 | +As seen before, "myenv" is the juju environment, as it is specified in your |
145 | +`~/.juju/environments.yaml`, that will be bootstrapped before running the |
146 | +tests and destroyed at the end of the test run. |
147 | |
148 | #### LXC #### |
149 | |
150 | @@ -109,33 +92,34 @@ |
151 | for these tests. At this time, we recommend using other environments, such as |
152 | OpenStack; but we will periodically check the tests in LXC environments |
153 | because it would be great to be able to use it. If you do want to use LXC, |
154 | -you will need to install the `apt-cacher-ng` and `lxc` packages. |
155 | - |
156 | -Currently running tests on a local environment is quite slow (with quantal |
157 | -host and precise container at least), so you may want to further increase the |
158 | -`jitsu test` command timeout. |
159 | - |
160 | -If Jitsu generates errors about not being able to bootstrap: |
161 | - |
162 | - CalledProcessError: Command '['juju', 'bootstrap']'... |
163 | - |
164 | -or if it hangs, then you may need to bootstrap the environment yourself and |
165 | -pass the --no-bootstrap switch to Jitsu. |
166 | +you will need to install the `apt-cacher-ng` and `lxc` packages. Also note |
167 | +that at this time juju-core does not support the local provider. |
168 | |
169 | ## Running the Charm From Development ## |
170 | |
171 | -If you have set up your environment to run functional tests, you also have set |
172 | -it up to run your local development charm. Developing and debugging with this |
173 | -is much easier than trying to develop and debug with the tests, unfortunately. |
174 | - |
175 | -To get started, first, simply do a `juju bootstrap`. Using a non-LXC |
176 | -environment probably will reduce frustrations. Then, deploy your charm like |
177 | -this (again, assuming you have set up your repo the way the functional tests |
178 | -need them, as described above). |
179 | - |
180 | +If you have set up your environment to run your local development charm, |
181 | +deploying the charm fails if attempted after the testing virtualenv has been |
182 | +created: juju deploy exits with an error due to ".venv" directory containing |
183 | +an absolute symbolic link. There are two ways to work around this problem. |
184 | + |
185 | +The first one is running "make clean" before deploying the charm: |
186 | + |
187 | + make clean |
188 | + juju bootstrap |
189 | juju deploy --repository=/path/to/charm/repo local:precise/juju-gui |
190 | juju expose juju-gui |
191 | |
192 | +The second one is just running "make deploy": |
193 | + |
194 | + juju bootstrap |
195 | + make deploy |
196 | + |
197 | +The "make deploy" command creates a temporary Juju repository (excluding |
198 | +the ".venv" directory), deploys the Juju GUI charm from that repository and |
199 | +exposes the juju-gui service. Also note that "make deploy" does not require |
200 | +you to manually set up a local Juju environment, and preserves, if already |
201 | +created, the testing virtualenv. |
202 | + |
203 | Now you are working with a test run, as described in |
204 | <https://juju.ubuntu.com/docs/write-charm.html#test-run>. The |
205 | `juju debug-hooks` command, described in the same web page, is your most |
206 | |
207 | === added file 'Makefile' |
208 | --- Makefile 1970-01-01 00:00:00 +0000 |
209 | +++ Makefile 2013-06-10 14:12:23 +0000 |
210 | @@ -0,0 +1,49 @@ |
211 | +# Juju GUI Makefile. |
212 | + |
213 | +JUJUTEST = juju-test --timeout=30m -v -e "$(JUJU_ENV)" --upload-tools |
214 | +VENV = ./tests/.venv |
215 | + |
216 | +all: setup |
217 | + |
218 | +setup: |
219 | + @./tests/00-setup |
220 | + |
221 | +unittest: setup |
222 | + ./tests/10-unit.test |
223 | + |
224 | +ftest: setup |
225 | + $(JUJUTEST) 20-functional.test |
226 | + |
227 | +# This will be eventually removed when we have juju-test --clean-state. |
228 | +test: unittest ftest |
229 | + |
230 | +# This will be eventually renamed as test when we have juju-test --clean-state. |
231 | +jujutest: |
232 | + $(JUJUTEST) |
233 | + |
234 | +lint: setup |
235 | + @flake8 --show-source --exclude=.venv ./hooks/ ./tests/ |
236 | + |
237 | +clean: |
238 | + find . -name '*.pyc' -delete |
239 | + rm -rf $(VENV) |
240 | + |
241 | +deploy: setup |
242 | + $(VENV)/bin/python ./tests/deploy.py |
243 | + |
244 | +help: |
245 | + @echo -e 'Juju GUI charm - list of make targets:\n' |
246 | + @echo -e 'make - Set up development and testing environment.\n' |
247 | + @echo 'make test JUJU_ENV="my-juju-env" - Run functional and unit tests.' |
248 | + @echo -e ' JUJU_ENV is the Juju environment that will be bootstrapped.\n' |
249 | + @echo -e 'make unittest - Run unit tests.\n' |
250 | + @echo 'make ftest JUJU_ENV="my-juju-env" - Run functional tests.' |
251 | + @echo -e ' JUJU_ENV is the Juju environment that will be bootstrapped.\n' |
252 | + @echo -e 'make lint - Run linter and pep8.\n' |
253 | + @echo -e 'make clean - Remove bytecode files and virtualenvs.\n' |
254 | + @echo 'make deploy [JUJU_ENV="my-juju-env]" - Deploy and expose the Juju' |
255 | + @echo ' GUI charm setting up a temporary Juju repository. Wait for the' |
256 | + @echo ' service to be started. If JUJU_ENV is not passed, the charm will' |
257 | + @echo ' be deployed in the default Juju environment.' |
258 | + |
259 | +.PHONY: all clean deploy ftest help jujutest lint setup test unittest |
260 | |
261 | === modified file 'revision' |
262 | --- revision 2013-06-06 13:51:45 +0000 |
263 | +++ revision 2013-06-10 14:12:23 +0000 |
264 | @@ -1,1 +1,1 @@ |
265 | -49 |
266 | +50 |
267 | |
268 | === added file 'tests/00-setup' |
269 | --- tests/00-setup 1970-01-01 00:00:00 +0000 |
270 | +++ tests/00-setup 2013-06-10 14:12:23 +0000 |
271 | @@ -0,0 +1,20 @@ |
272 | +#!/bin/sh |
273 | + |
274 | +TESTDIR=`dirname $0` |
275 | +VENV="$TESTDIR/.venv" |
276 | +ACTIVATE="$VENV/bin/activate" |
277 | +REQUIREMENTS="$TESTDIR/requirements.pip" |
278 | + |
279 | + |
280 | +createvenv() { |
281 | + # Create a virtualenv if it does not exist, or it is older than requirements. |
282 | + if [ ! -f "$ACTIVATE" -o "$REQUIREMENTS" -nt "$ACTIVATE" ]; then |
283 | + virtualenv --distribute $VENV |
284 | + . $VENV/bin/activate && \ |
285 | + yes w | pip install --use-mirrors -r $REQUIREMENTS |
286 | + touch $VENV/bin/activate |
287 | + fi |
288 | +} |
289 | + |
290 | + |
291 | +createvenv |
292 | |
293 | === renamed file 'tests/unit.test' => 'tests/10-unit.test' |
294 | --- tests/unit.test 2012-12-07 21:16:55 +0000 |
295 | +++ tests/10-unit.test 2013-06-10 14:12:23 +0000 |
296 | @@ -1,5 +1,6 @@ |
297 | -#!/usr/bin/env python2 |
298 | -# -*- python -*- |
299 | +#!tests/.venv/bin/python |
300 | + |
301 | +"""Unit test suite.""" |
302 | |
303 | import os |
304 | import sys |
305 | @@ -8,4 +9,5 @@ |
306 | |
307 | runner = unittest.TextTestRunner(verbosity=2) |
308 | suite = unittest.TestLoader().discover(os.path.dirname(__file__)) |
309 | -sys.exit(runner.run(suite)) |
310 | +result = runner.run(suite) |
311 | +sys.exit(not result.wasSuccessful()) |
312 | |
313 | === renamed file 'tests/deploy.test' => 'tests/20-functional.test' |
314 | --- tests/deploy.test 2013-04-27 18:57:12 +0000 |
315 | +++ tests/20-functional.test 2013-06-10 14:12:23 +0000 |
316 | @@ -1,30 +1,31 @@ |
317 | -#!/usr/bin/env python2 |
318 | -#-*- python -*- |
319 | +#!tests/.venv/bin/python |
320 | |
321 | -import os |
322 | import unittest |
323 | import urlparse |
324 | |
325 | -from charmhelpers import make_charm_config_file |
326 | from selenium.webdriver import Firefox |
327 | from selenium.webdriver.support import ui |
328 | -from shelltoolbox import command |
329 | from xvfbwrapper import Xvfb |
330 | |
331 | - |
332 | -JUJU_GUI_SOURCE = os.getenv('JUJU_GUI_SOURCE') |
333 | +from deploy import juju_deploy |
334 | +from helpers import ( |
335 | + juju_destroy_service, |
336 | + legacy_juju, |
337 | + ssh, |
338 | +) |
339 | + |
340 | + |
341 | JUJU_GUI_TEST_BRANCH = 'lp:~juju-gui/juju-gui/charm-tests-branch' |
342 | STAGING_SERVICES = ('haproxy', 'mediawiki', 'memcached', 'mysql', 'wordpress') |
343 | -jitsu = command('jitsu') |
344 | -juju = command('juju') |
345 | -ssh = command('ssh') |
346 | +is_legacy_juju = legacy_juju() |
347 | |
348 | |
349 | class DeployTestMixin(object): |
350 | |
351 | + charm = 'juju-gui' |
352 | + port = '443' |
353 | + |
354 | def setUp(self): |
355 | - self.charm = 'juju-gui' |
356 | - self.port = '443' |
357 | # Perform all graphical operations in memory. |
358 | vdisplay = Xvfb(width=1280, height=720) |
359 | vdisplay.start() |
360 | @@ -34,7 +35,7 @@ |
361 | self.addCleanup(selenium.quit) |
362 | |
363 | def tearDown(self): |
364 | - juju('destroy-service', self.charm) |
365 | + juju_destroy_service(self.charm) |
366 | |
367 | def assertEnvironmentIsConnected(self): |
368 | """Assert the GUI environment is connected to the Juju API agent.""" |
369 | @@ -42,21 +43,6 @@ |
370 | 'return app.env.get("connected");', |
371 | error='Environment not connected.') |
372 | |
373 | - def make_config_file(self, options=None): |
374 | - """Create a charm config file adding, if required, the Juju GUI source. |
375 | - |
376 | - Return the created config file object. |
377 | - |
378 | - The Juju GUI source can be provided by setting the environment variable |
379 | - *JUJU_GUI_SOURCE*. This is just a simple wrapper around |
380 | - ``charmhelpers.make_charm_config_file``. |
381 | - """ |
382 | - if options is None: |
383 | - options = {} |
384 | - if JUJU_GUI_SOURCE is not None: |
385 | - options.setdefault('juju-gui-source', JUJU_GUI_SOURCE) |
386 | - return make_charm_config_file({self.charm: options}) |
387 | - |
388 | def handle_browser_warning(self): |
389 | """Overstep the browser warning dialog if required.""" |
390 | self.wait_for_script( |
391 | @@ -115,14 +101,6 @@ |
392 | condition = lambda driver: driver.execute_script(script) |
393 | return self.wait_for(condition, error=error, timeout=timeout) |
394 | |
395 | - def login(self, password): |
396 | - """Log in to access the Juju GUI using the provided *password*.""" |
397 | - form = self.wait_for_css_selector('form', 'Login form not found.') |
398 | - passwd = form.find_element_by_css_selector('input[type=password]') |
399 | - passwd.send_keys(password) |
400 | - submit = form.find_element_by_css_selector('input[type=submit]') |
401 | - submit.click() |
402 | - |
403 | def get_service_names(self): |
404 | """Return the set of services' names displayed in the current page.""" |
405 | def services_found(driver): |
406 | @@ -135,8 +113,8 @@ |
407 | # Just invoking ``juju destroy-service juju-gui`` in tearDown |
408 | # should execute the ``stop`` hook, stopping all the services |
409 | # started by the charm in the machine. Right now this does not |
410 | - # work, so the same behavior is accomplished keeping track of |
411 | - # started services and manually stopping them here. |
412 | + # work in pyJuju, so the same behavior is accomplished keeping |
413 | + # track of started services and manually stopping them here. |
414 | target = 'ubuntu@{0}'.format(hostname) |
415 | for service in services: |
416 | ssh(target, 'sudo', 'service', service, 'stop') |
417 | @@ -144,36 +122,24 @@ |
418 | |
419 | class DeployTest(DeployTestMixin, unittest.TestCase): |
420 | |
421 | - def deploy(self, config=None): |
422 | - """Deploy and expose the Juju GUI charm. Return the service host name. |
423 | - |
424 | - Also wait until the service is started. |
425 | - If *config_path* is provided, it will be used when deploying the charm. |
426 | - """ |
427 | - config_file = self.make_config_file(config) |
428 | - juju('deploy', 'local:{0}'.format(self.charm), |
429 | - '--config', config_file.name) |
430 | - juju('expose', self.charm) |
431 | - jitsu( |
432 | - 'watch', '--failfast', self.charm, |
433 | - '--state', 'started', '--open-port', self.port) |
434 | - address = jitsu('get-service-info', self.charm, 'public-address') |
435 | - return address.strip().split(':')[1] |
436 | - |
437 | def test_api_agent(self): |
438 | # Ensure the Juju GUI and API agent services are correctly set up. |
439 | - hostname = self.deploy() |
440 | - # XXX 2012-11-29 frankban bug=872264: see *stop_services* above. |
441 | - self.addCleanup( |
442 | - self.stop_services, |
443 | - hostname, ['haproxy', 'apache2', 'juju-api-agent']) |
444 | + unit_info = juju_deploy(self.charm) |
445 | + hostname = unit_info['public-address'] |
446 | + if is_legacy_juju: |
447 | + # XXX 2012-11-29 frankban bug=872264: see *stop_services* above. |
448 | + self.addCleanup( |
449 | + self.stop_services, |
450 | + hostname, ['haproxy', 'apache2', 'juju-api-agent']) |
451 | self.navigate_to(hostname) |
452 | self.handle_browser_warning() |
453 | self.assertEnvironmentIsConnected() |
454 | |
455 | + @unittest.skipUnless(is_legacy_juju, 'staging only works in pyJuju') |
456 | def test_staging(self): |
457 | # Ensure the Juju GUI and improv services are correctly set up. |
458 | - hostname = self.deploy({'staging': 'true'}) |
459 | + unit_info = juju_deploy(self.charm, options={'staging': 'true'}) |
460 | + hostname = unit_info['public-address'] |
461 | # XXX 2012-11-29 frankban bug=872264: see *stop_services* above. |
462 | self.addCleanup( |
463 | self.stop_services, |
464 | @@ -186,44 +152,24 @@ |
465 | |
466 | def test_branch_source(self): |
467 | # Ensure the Juju GUI is correctly deployed from a Bazaar branch. |
468 | - hostname = self.deploy({'juju-gui-source': JUJU_GUI_TEST_BRANCH}) |
469 | - # XXX 2012-11-29 frankban bug=872264: see *stop_services* above. |
470 | - self.addCleanup( |
471 | - self.stop_services, |
472 | - hostname, ['haproxy', 'apache2', 'juju-api-agent']) |
473 | + unit_info = juju_deploy( |
474 | + self.charm, options={'juju-gui-source': JUJU_GUI_TEST_BRANCH}) |
475 | + hostname = unit_info['public-address'] |
476 | + if is_legacy_juju: |
477 | + # XXX 2012-11-29 frankban bug=872264: see *stop_services* above. |
478 | + self.addCleanup( |
479 | + self.stop_services, |
480 | + hostname, ['haproxy', 'apache2', 'juju-api-agent']) |
481 | self.navigate_to(hostname) |
482 | self.handle_browser_warning() |
483 | self.assertEnvironmentIsConnected() |
484 | |
485 | - |
486 | -class DeployToTest(DeployTestMixin, unittest.TestCase): |
487 | - |
488 | - def deploy_to(self): |
489 | - """Deploy the Juju GUI charm in the Juju bootstrap node. |
490 | - |
491 | - Expose it and return the service host name. |
492 | - Also wait until the service is started. |
493 | - """ |
494 | - config_file = self.make_config_file() |
495 | - # The id of the bootstrap node is 0 (the first node started by Juju). |
496 | - jitsu('deploy-to', '0', 'local:{0}'.format(self.charm), |
497 | - '--config', config_file.name) |
498 | - juju('expose', self.charm) |
499 | - jitsu( |
500 | - 'watch', '--failfast', self.charm, |
501 | - '--state', 'started', '--open-port', self.port) |
502 | - address = jitsu('get-service-info', self.charm, 'public-address') |
503 | - return address.strip().split(':')[1] |
504 | - |
505 | - def test_api_agent(self): |
506 | - # Ensure the Juju GUI and API agent services are correctly set up in |
507 | - # the Juju bootstrap node. |
508 | - hostname = self.deploy_to() |
509 | - # XXX 2012-11-29 frankban bug=872264: see *stop_services* above. |
510 | - self.addCleanup( |
511 | - self.stop_services, |
512 | - hostname, ['haproxy', 'apache2', 'juju-api-agent']) |
513 | - self.navigate_to(hostname) |
514 | + @unittest.skipIf(is_legacy_juju, 'force-machine only works in juju-core') |
515 | + def test_force_machine(self): |
516 | + # Ensure the Juju GUI is correctly set up in the Juju bootstrap node. |
517 | + unit_info = juju_deploy(self.charm, force_machine=0) |
518 | + self.assertEqual('0', unit_info['machine']) |
519 | + self.navigate_to(unit_info['public-address']) |
520 | self.handle_browser_warning() |
521 | self.assertEnvironmentIsConnected() |
522 | |
523 | |
524 | === added file 'tests/deploy.py' |
525 | --- tests/deploy.py 1970-01-01 00:00:00 +0000 |
526 | +++ tests/deploy.py 2013-06-10 14:12:23 +0000 |
527 | @@ -0,0 +1,62 @@ |
528 | +"""Juju GUI deploy helper.""" |
529 | + |
530 | +from __future__ import print_function |
531 | +import json |
532 | +import os |
533 | +import tempfile |
534 | + |
535 | +from charmhelpers import make_charm_config_file |
536 | + |
537 | +from helpers import ( |
538 | + command, |
539 | + juju, |
540 | + wait_for_unit, |
541 | +) |
542 | + |
543 | + |
544 | +rsync = command('rsync', '-a', '--exclude', '.bzr', '--exclude', '.venv') |
545 | + |
546 | + |
547 | +def setup_repository(source, series='precise'): |
548 | + """Create a temporary Juju repository to use for charm deployment. |
549 | + |
550 | + Copy the charm files in source in the precise repository section, excluding |
551 | + the virtualenv and Bazaar directories. |
552 | + |
553 | + Return the repository path. |
554 | + """ |
555 | + source = os.path.abspath(source) |
556 | + repo = tempfile.mkdtemp() |
557 | + destination = os.path.join(repo, series) |
558 | + os.makedirs(destination) |
559 | + rsync(source, destination) |
560 | + return repo |
561 | + |
562 | + |
563 | +def juju_deploy(charm, options=None, force_machine=None, charm_source=None): |
564 | + """Deploy and expose the charm. Return the first unit's public address. |
565 | + |
566 | + Also wait until the service is exposed and the first unit started. |
567 | + If options are provided, they will be used when deploying the charm. |
568 | + If force_machine is not None, create the unit in the specified machine. |
569 | + If charm_source is None, dynamically retrieve the charm source directory. |
570 | + """ |
571 | + if charm_source is None: |
572 | + # Dynamically retrieve the charm source based on the path of this file. |
573 | + charm_source = os.path.join(os.path.dirname(__file__), '..') |
574 | + repo = setup_repository(charm_source) |
575 | + args = ['deploy', '--repository', repo] |
576 | + if options is not None: |
577 | + config_file = make_charm_config_file({charm: options}) |
578 | + args.extend(['--config', config_file.name]) |
579 | + if force_machine is not None: |
580 | + args.extend(['--force-machine', str(force_machine)]) |
581 | + args.append('local:{0}'.format(charm)) |
582 | + juju(*args) |
583 | + juju('expose', charm) |
584 | + return wait_for_unit(charm) |
585 | + |
586 | + |
587 | +if __name__ == '__main__': |
588 | + unit = juju_deploy('juju-gui') |
589 | + print(json.dumps(unit, indent=2)) |
590 | |
591 | === added file 'tests/helpers.py' |
592 | --- tests/helpers.py 1970-01-01 00:00:00 +0000 |
593 | +++ tests/helpers.py 2013-06-10 14:12:23 +0000 |
594 | @@ -0,0 +1,145 @@ |
595 | +"""Juju GUI test helpers.""" |
596 | + |
597 | +from functools import wraps |
598 | +import json |
599 | +import os |
600 | +import subprocess |
601 | +import time |
602 | + |
603 | + |
604 | +class ProcessError(subprocess.CalledProcessError): |
605 | + """Error running a shell command.""" |
606 | + |
607 | + def __init__(self, retcode, cmd, output, error): |
608 | + super(ProcessError, self).__init__(retcode, cmd, output) |
609 | + self.error = error |
610 | + |
611 | + def __str__(self): |
612 | + msg = super(ProcessError, self).__str__() |
613 | + return '{}. Output: {!r}. Error: {!r}.'.format( |
614 | + msg, self.output, self.error) |
615 | + |
616 | + |
617 | +def command(*base_args): |
618 | + """Return a callable that will run the given command with any arguments. |
619 | + |
620 | + The first argument is the path to the command to run, subsequent arguments |
621 | + are command-line arguments to "bake into" the returned callable. |
622 | + |
623 | + The callable runs the given executable and also takes arguments that will |
624 | + be appended to the "baked in" arguments. |
625 | + |
626 | + For example, this code will list a file named "foo" (if it exists): |
627 | + |
628 | + ls_foo = command('/bin/ls', 'foo') |
629 | + ls_foo() |
630 | + |
631 | + While this invocation will list "foo" and "bar" (assuming they exist): |
632 | + |
633 | + ls_foo('bar') |
634 | + """ |
635 | + PIPE = subprocess.PIPE |
636 | + |
637 | + def runner(*args, **kwargs): |
638 | + cmd = base_args + args |
639 | + process = subprocess.Popen(cmd, stdout=PIPE, stderr=PIPE, **kwargs) |
640 | + output, error = process.communicate() |
641 | + retcode = process.poll() |
642 | + if retcode: |
643 | + raise ProcessError(retcode, cmd, output, error) |
644 | + return output |
645 | + |
646 | + return runner |
647 | + |
648 | + |
649 | +juju_command = command('juju') |
650 | +juju_env = lambda: os.getenv('JUJU_ENV') # This is propagated by juju-test. |
651 | +ssh = command('ssh') |
652 | + |
653 | + |
654 | +def legacy_juju(): |
655 | + """Return True if pyJuju is being used, False otherwise.""" |
656 | + try: |
657 | + juju_command('--version') |
658 | + except ProcessError: |
659 | + return False |
660 | + return True |
661 | + |
662 | + |
663 | +def retry(exception, tries=10, delay=1): |
664 | + """If the decorated function raises the exception, wait and try it again. |
665 | + |
666 | + Raise the exception raised by the last call if the function does not |
667 | + exit normally after the specified number of tries. |
668 | + |
669 | + Original from http://wiki.python.org/moin/PythonDecoratorLibrary#Retry. |
670 | + """ |
671 | + def decorator(func): |
672 | + @wraps(func) |
673 | + def decorated(*args, **kwargs): |
674 | + mtries = tries |
675 | + while mtries: |
676 | + try: |
677 | + return func(*args, **kwargs) |
678 | + except exception as err: |
679 | + time.sleep(delay) |
680 | + mtries -= 1 |
681 | + raise err |
682 | + return decorated |
683 | + return decorator |
684 | + |
685 | + |
686 | +@retry(ProcessError) |
687 | +def juju(command, *args): |
688 | + """Call the juju command, passing the environment parameters if required. |
689 | + |
690 | + The environment value can be provided in args, or can be found in the |
691 | + context as JUJU_ENV. |
692 | + """ |
693 | + arguments = [command] |
694 | + if ('-e' not in args) and ('--environment' not in args): |
695 | + env = juju_env() |
696 | + if env is not None: |
697 | + arguments.extend(['-e', env]) |
698 | + arguments.extend(args) |
699 | + return juju_command(*arguments) |
700 | + |
701 | + |
702 | +def juju_destroy_service(service): |
703 | + """Destroy the given service and wait for the service to be removed.""" |
704 | + juju('destroy-service', service) |
705 | + while True: |
706 | + services = juju_status().get('services', {}) |
707 | + if service not in services: |
708 | + return |
709 | + |
710 | + |
711 | +def juju_status(): |
712 | + """Return the Juju status as a dictionary.""" |
713 | + status = juju('status', '--format', 'json') |
714 | + return json.loads(status) |
715 | + |
716 | + |
717 | +def wait_for_unit(sevice): |
718 | + """Wait for the first unit of the given service to be started. |
719 | + |
720 | + Also wait for the service to be exposed. |
721 | + Raise a RuntimeError if the unit is found in an error state. |
722 | + Return info about the first unit as a dict containing at least the |
723 | + following keys: agent-state, machine, and public-address. |
724 | + """ |
725 | + while True: |
726 | + status = juju_status() |
727 | + service = status.get('services', {}).get(sevice) |
728 | + if service is None or not service.get('exposed'): |
729 | + continue |
730 | + units = service.get('units', {}) |
731 | + if not len(units): |
732 | + continue |
733 | + unit = units.values()[0] |
734 | + state = unit['agent-state'] |
735 | + if 'error' in state: |
736 | + raise RuntimeError( |
737 | + 'the service unit is in an error state: {}'.format(state)) |
738 | + if state == 'started': |
739 | + return unit |
740 | |
741 | === added file 'tests/requirements.pip' |
742 | --- tests/requirements.pip 1970-01-01 00:00:00 +0000 |
743 | +++ tests/requirements.pip 2013-06-10 14:12:23 +0000 |
744 | @@ -0,0 +1,10 @@ |
745 | +-e bzr+http://launchpad.net/charm-tools#egg=charm-tools |
746 | +flake8==2.0 |
747 | +launchpadlib==1.10.2 |
748 | +mock==1.0.1 |
749 | +python-apt==0.8.5 |
750 | +-e bzr+http://launchpad.net/python-shelltoolbox#egg=python-shelltoolbox |
751 | +PyYAML==3.10 |
752 | +selenium==2.33.0 |
753 | +Tempita==0.5.1 |
754 | +xvfbwrapper==0.2.2 |
755 | |
756 | === modified file 'tests/test_backends.py' |
757 | --- tests/test_backends.py 2013-06-06 13:51:45 +0000 |
758 | +++ tests/test_backends.py 2013-06-10 14:12:23 +0000 |
759 | @@ -192,14 +192,16 @@ |
760 | def test_start_agent(self): |
761 | test_backend = backend.Backend(config=self.alwaysFalse) |
762 | test_backend.start() |
763 | - for mocked in ('service_control', 'start_agent', 'start_gui', |
764 | + for mocked in ( |
765 | + 'service_control', 'start_agent', 'start_gui', |
766 | 'open_port', 'su'): |
767 | self.assertTrue(mocked, '{} was not called'.format(mocked)) |
768 | |
769 | def test_start_improv(self): |
770 | test_backend = backend.Backend(config=self.alwaysTrue) |
771 | test_backend.start() |
772 | - for mocked in ('service_control', 'start_improv', 'start_gui', |
773 | + for mocked in ( |
774 | + 'service_control', 'start_improv', 'start_gui', |
775 | 'open_port', 'su'): |
776 | self.assertTrue(mocked, '{} was not called'.format(mocked)) |
777 | |
778 | |
779 | === added file 'tests/test_deploy.py' |
780 | --- tests/test_deploy.py 1970-01-01 00:00:00 +0000 |
781 | +++ tests/test_deploy.py 2013-06-10 14:12:23 +0000 |
782 | @@ -0,0 +1,163 @@ |
783 | +"""Juju GUI deploy module tests.""" |
784 | + |
785 | +import os |
786 | +import shutil |
787 | +import tempfile |
788 | +import unittest |
789 | + |
790 | +import mock |
791 | + |
792 | +from deploy import ( |
793 | + juju_deploy, |
794 | + setup_repository, |
795 | +) |
796 | + |
797 | + |
798 | +class TestSetupRepository(unittest.TestCase): |
799 | + |
800 | + def setUp(self): |
801 | + # Create a directory structure for the charm source. |
802 | + self.source = tempfile.mkdtemp() |
803 | + self.charm_name = os.path.basename(self.source) |
804 | + self.addCleanup(shutil.rmtree, self.source) |
805 | + # Create a file in the source dir. |
806 | + _, self.root_file = tempfile.mkstemp(dir=self.source) |
807 | + # Create a Bazaar repository directory with a file in it. |
808 | + bzr_dir = os.path.join(self.source, '.bzr') |
809 | + os.mkdir(bzr_dir) |
810 | + tempfile.mkstemp(dir=bzr_dir) |
811 | + # Create a tests directory including a .venv directory and a file. |
812 | + self.tests_dir = os.path.join(self.source, 'tests') |
813 | + venv_dir = os.path.join(self.tests_dir, '.venv') |
814 | + os.makedirs(venv_dir) |
815 | + tempfile.mkstemp(dir=venv_dir) |
816 | + # Create a test file. |
817 | + _, self.tests_file = tempfile.mkstemp(dir=self.tests_dir) |
818 | + |
819 | + def assert_dir_exists(self, path): |
820 | + self.assertTrue( |
821 | + os.path.isdir(path), |
822 | + 'the directory {!r} does not exist'.format(path)) |
823 | + |
824 | + def assert_files_equal(self, expected, path): |
825 | + fileset = set() |
826 | + for dirpath, _, filenames in os.walk(path): |
827 | + relpath = os.path.relpath(dirpath, path) |
828 | + if relpath == '.': |
829 | + relpath = '' |
830 | + else: |
831 | + fileset.add(relpath + os.path.sep) |
832 | + fileset.update(os.path.join(relpath, name) for name in filenames) |
833 | + self.assertEqual(expected, fileset) |
834 | + |
835 | + def check_repository(self, repo, series): |
836 | + # The repository has been created in the temp directory. |
837 | + self.assertEqual(tempfile.tempdir, os.path.split(repo)[0]) |
838 | + self.assert_dir_exists(repo) |
839 | + # The repository only contains the series directory. |
840 | + self.assertEqual([series], os.listdir(repo)) |
841 | + series_dir = os.path.join(repo, series) |
842 | + self.assert_dir_exists(series_dir) |
843 | + # The series directory only contains our charm. |
844 | + self.assertEqual([self.charm_name], os.listdir(series_dir)) |
845 | + self.assert_dir_exists(os.path.join(series_dir, self.charm_name)) |
846 | + |
847 | + def test_repository(self): |
848 | + # The charm repository is correctly created with the default series. |
849 | + repo = setup_repository(self.source) |
850 | + self.check_repository(repo, 'precise') |
851 | + |
852 | + def test_series(self): |
853 | + # The charm repository is created with the given series. |
854 | + repo = setup_repository(self.source, series='raring') |
855 | + self.check_repository(repo, 'raring') |
856 | + |
857 | + def test_charm_files(self): |
858 | + # The charm files are correctly copied inside the repository, excluding |
859 | + # unwanted directories. |
860 | + repo = setup_repository(self.source) |
861 | + charm_dir = os.path.join(repo, 'precise', self.charm_name) |
862 | + test_dir_name = os.path.basename(self.tests_dir) |
863 | + expected = set([ |
864 | + os.path.basename(self.root_file), |
865 | + test_dir_name + os.path.sep, |
866 | + os.path.join(test_dir_name, os.path.basename(self.tests_file)) |
867 | + ]) |
868 | + self.assert_files_equal(expected, charm_dir) |
869 | + |
870 | + |
871 | +class TestJujuDeploy(unittest.TestCase): |
872 | + |
873 | + unit_info = {'public-address': 'unit.example.com'} |
874 | + charm = 'test-charm' |
875 | + expose_call = mock.call('expose', charm) |
876 | + local_charm = 'local:{}'.format(charm) |
877 | + repo = '/tmp/repo/' |
878 | + |
879 | + @mock.patch('deploy.juju') |
880 | + @mock.patch('deploy.wait_for_unit') |
881 | + @mock.patch('deploy.setup_repository') |
882 | + def call_deploy( |
883 | + self, mock_setup_repository, mock_wait_for_unit, mock_juju, |
884 | + **kwargs): |
885 | + mock_setup_repository.return_value = self.repo |
886 | + mock_wait_for_unit.return_value = self.unit_info |
887 | + charm_source = kwargs.setdefault( |
888 | + 'charm_source', os.path.join(os.path.dirname(__file__), '..')) |
889 | + unit_info = juju_deploy(self.charm, **kwargs) |
890 | + mock_setup_repository.assert_called_once_with(charm_source) |
891 | + # The unit address is correctly returned. |
892 | + self.assertEqual(self.unit_info, unit_info) |
893 | + self.assertEqual(1, mock_wait_for_unit.call_count) |
894 | + # Juju is called two times: deploy and expose. |
895 | + juju_calls = mock_juju.call_args_list |
896 | + self.assertEqual(2, len(juju_calls)) |
897 | + deploy_call, expose_call = juju_calls |
898 | + self.assertEqual(self.expose_call, expose_call) |
899 | + return deploy_call |
900 | + |
901 | + def test_deployment(self): |
902 | + # The function deploys and exposes the given charm. |
903 | + expected_deploy_call = mock.call( |
904 | + 'deploy', |
905 | + '--repository', self.repo, |
906 | + self.local_charm, |
907 | + ) |
908 | + deploy_call = self.call_deploy() |
909 | + self.assertEqual(expected_deploy_call, deploy_call) |
910 | + |
911 | + def test_options(self): |
912 | + # The function handles charm options. |
913 | + mock_config_file = mock.Mock() |
914 | + mock_config_file.name = '/tmp/config.yaml' |
915 | + expected_deploy_call = mock.call( |
916 | + 'deploy', |
917 | + '--repository', self.repo, |
918 | + '--config', mock_config_file.name, |
919 | + self.local_charm, |
920 | + ) |
921 | + with mock.patch('deploy.make_charm_config_file') as mock_callable: |
922 | + mock_callable.return_value = mock_config_file |
923 | + deploy_call = self.call_deploy(options={'foo': 'bar'}) |
924 | + self.assertEqual(expected_deploy_call, deploy_call) |
925 | + |
926 | + def test_force_machine(self): |
927 | + # The function can deploy charms in a specified machine. |
928 | + expected_deploy_call = mock.call( |
929 | + 'deploy', |
930 | + '--repository', self.repo, |
931 | + '--force-machine', '42', |
932 | + self.local_charm, |
933 | + ) |
934 | + deploy_call = self.call_deploy(force_machine=42) |
935 | + self.assertEqual(expected_deploy_call, deploy_call) |
936 | + |
937 | + def test_charm_source(self): |
938 | + # The function can deploy a charm from a specific source. |
939 | + expected_deploy_call = mock.call( |
940 | + 'deploy', |
941 | + '--repository', self.repo, |
942 | + self.local_charm, |
943 | + ) |
944 | + deploy_call = self.call_deploy(charm_source='/tmp/source/') |
945 | + self.assertEqual(expected_deploy_call, deploy_call) |
946 | |
947 | === added file 'tests/test_helpers.py' |
948 | --- tests/test_helpers.py 1970-01-01 00:00:00 +0000 |
949 | +++ tests/test_helpers.py 2013-06-10 14:12:23 +0000 |
950 | @@ -0,0 +1,327 @@ |
951 | +"""Juju GUI helpers tests.""" |
952 | + |
953 | +import json |
954 | +import unittest |
955 | + |
956 | +import mock |
957 | + |
958 | +from helpers import ( |
959 | + command, |
960 | + juju, |
961 | + juju_destroy_service, |
962 | + juju_env, |
963 | + juju_status, |
964 | + legacy_juju, |
965 | + ProcessError, |
966 | + retry, |
967 | + wait_for_unit, |
968 | +) |
969 | + |
970 | + |
971 | +class TestCommand(unittest.TestCase): |
972 | + |
973 | + def test_simple_command(self): |
974 | + # Creating a simple command (ls) works and running the command |
975 | + # produces a string. |
976 | + ls = command('/bin/ls') |
977 | + self.assertIsInstance(ls(), str) |
978 | + |
979 | + def test_arguments(self): |
980 | + # Arguments can be passed to commands. |
981 | + ls = command('/bin/ls') |
982 | + self.assertIn('Usage:', ls('--help')) |
983 | + |
984 | + def test_missing(self): |
985 | + # If the command does not exist, an OSError (No such file or |
986 | + # directory) is raised. |
987 | + bad = command('this command does not exist') |
988 | + with self.assertRaises(OSError) as info: |
989 | + bad() |
990 | + self.assertEqual(2, info.exception.errno) |
991 | + |
992 | + def test_error(self): |
993 | + # If the command returns a non-zero exit code, an exception is raised. |
994 | + bad = command('/bin/ls', '--not a valid switch') |
995 | + self.assertRaises(ProcessError, bad) |
996 | + |
997 | + def test_baked_in_arguments(self): |
998 | + # Arguments can be passed when creating the command as well as when |
999 | + # executing it. |
1000 | + ll = command('/bin/ls', '-al') |
1001 | + self.assertIn('rw', ll()) # Assumes a file is r/w in the pwd. |
1002 | + self.assertIn('Usage:', ll('--help')) |
1003 | + |
1004 | + def test_quoting(self): |
1005 | + # There is no need to quote special shell characters in commands. |
1006 | + ls = command('/bin/ls') |
1007 | + ls('--help', '>') |
1008 | + |
1009 | + |
1010 | +@mock.patch('helpers.juju_command') |
1011 | +class TestJuju(unittest.TestCase): |
1012 | + |
1013 | + env = 'test-env' |
1014 | + patch_environ = mock.patch('os.environ', {'JUJU_ENV': env}) |
1015 | + process_error = ProcessError(1, 'an error occurred', 'output', 'error') |
1016 | + |
1017 | + def test_e_in_args(self, mock_juju_command): |
1018 | + # The command includes the environment if provided with -e. |
1019 | + with self.patch_environ: |
1020 | + juju('deploy', '-e', 'another-env', 'test-charm') |
1021 | + mock_juju_command.assert_called_once_with( |
1022 | + 'deploy', '-e', 'another-env', 'test-charm') |
1023 | + |
1024 | + def test_environment_in_args(self, mock_juju_command): |
1025 | + # The command includes the environment if provided with --environment. |
1026 | + with self.patch_environ: |
1027 | + juju('deploy', '--environment', 'another-env', 'test-charm') |
1028 | + mock_juju_command.assert_called_once_with( |
1029 | + 'deploy', '--environment', 'another-env', 'test-charm') |
1030 | + |
1031 | + def test_environment_in_context(self, mock_juju_command): |
1032 | + # The command includes the environment if found in the context as |
1033 | + # the environment variable JUJU_ENV. |
1034 | + with self.patch_environ: |
1035 | + juju('deploy', 'test-charm') |
1036 | + mock_juju_command.assert_called_once_with( |
1037 | + 'deploy', '-e', self.env, 'test-charm') |
1038 | + |
1039 | + def test_environment_not_in_context(self, mock_juju_command): |
1040 | + # The command does not include the environment if not found in the |
1041 | + # context as the environment variable JUJU_ENV. |
1042 | + with mock.patch('os.environ', {}): |
1043 | + juju('deploy', 'test-charm') |
1044 | + mock_juju_command.assert_called_once_with('deploy', 'test-charm') |
1045 | + |
1046 | + def test_handle_process_errors(self, mock_juju_command): |
1047 | + # The command retries several times before failing if a ProcessError is |
1048 | + # raised. |
1049 | + mock_juju_command.side_effect = ([self.process_error] * 9) + ['value'] |
1050 | + with mock.patch('time.sleep') as mock_sleep: |
1051 | + with mock.patch('os.environ', {}): |
1052 | + result = juju('deploy', 'test-charm') |
1053 | + self.assertEqual('value', result) |
1054 | + self.assertEqual(10, mock_juju_command.call_count) |
1055 | + mock_juju_command.assert_called_with('deploy', 'test-charm') |
1056 | + self.assertEqual(9, mock_sleep.call_count) |
1057 | + mock_sleep.assert_called_with(1) |
1058 | + |
1059 | + def test_raise_process_errors(self, mock_juju_command): |
1060 | + # The command raises the last ProcessError after a number of retries. |
1061 | + mock_juju_command.side_effect = [self.process_error] * 10 |
1062 | + with mock.patch('time.sleep') as mock_sleep: |
1063 | + with mock.patch('os.environ', {}): |
1064 | + with self.assertRaises(ProcessError) as info: |
1065 | + juju('deploy', 'test-charm') |
1066 | + self.assertIs(self.process_error, info.exception) |
1067 | + self.assertEqual(10, mock_juju_command.call_count) |
1068 | + mock_juju_command.assert_called_with('deploy', 'test-charm') |
1069 | + self.assertEqual(10, mock_sleep.call_count) |
1070 | + mock_sleep.assert_called_with(1) |
1071 | + |
1072 | + |
1073 | +@mock.patch('helpers.juju') |
1074 | +@mock.patch('helpers.juju_status') |
1075 | +class TestJujuDestroyService(unittest.TestCase): |
1076 | + |
1077 | + service = 'test-service' |
1078 | + |
1079 | + def test_service_destroyed(self, mock_juju_status, mock_juju): |
1080 | + # The juju destroy-service command is correctly called. |
1081 | + mock_juju_status.return_value = {} |
1082 | + juju_destroy_service(self.service) |
1083 | + self.assertEqual(1, mock_juju_status.call_count) |
1084 | + mock_juju.assert_called_once_with('destroy-service', self.service) |
1085 | + |
1086 | + def test_wait_until_removed(self, mock_juju_status, mock_juju): |
1087 | + # The function waits for the service to be removed. |
1088 | + mock_juju_status.side_effect = ( |
1089 | + {'services': {self.service: {}, 'another-service': {}}}, |
1090 | + {'services': {'another-service': {}}}, |
1091 | + ) |
1092 | + juju_destroy_service(self.service) |
1093 | + self.assertEqual(2, mock_juju_status.call_count) |
1094 | + mock_juju.assert_called_once_with('destroy-service', self.service) |
1095 | + |
1096 | + |
1097 | +class TestJujuEnv(unittest.TestCase): |
1098 | + |
1099 | + def test_env_in_context(self): |
1100 | + # The function returns the juju env if found in the execution context. |
1101 | + with mock.patch('os.environ', {'JUJU_ENV': 'test-env'}): |
1102 | + self.assertEqual('test-env', juju_env()) |
1103 | + |
1104 | + def test_env_not_in_context(self): |
1105 | + # The function returns None if JUJU_ENV is not included in the context. |
1106 | + with mock.patch('os.environ', {}): |
1107 | + self.assertIsNone(juju_env()) |
1108 | + |
1109 | + |
1110 | +class TestJujuStatus(unittest.TestCase): |
1111 | + |
1112 | + status = { |
1113 | + 'machines': { |
1114 | + '0': {'agent-state': 'running', 'dns-name': 'ec2.example.com'}, |
1115 | + }, |
1116 | + 'services': { |
1117 | + 'juju-gui': {'charm': 'cs:precise/juju-gui-48', 'exposed': True}, |
1118 | + }, |
1119 | + } |
1120 | + |
1121 | + @mock.patch('helpers.juju') |
1122 | + def test_status(self, mock_juju): |
1123 | + # The function returns the unserialized juju status. |
1124 | + mock_juju.return_value = json.dumps(self.status) |
1125 | + status = juju_status() |
1126 | + self.assertEqual(self.status, status) |
1127 | + mock_juju.assert_called_once_with('status', '--format', 'json') |
1128 | + |
1129 | + |
1130 | +@mock.patch('helpers.juju_command') |
1131 | +class TestLegacyJuju(unittest.TestCase): |
1132 | + |
1133 | + def test_pyjuju(self, mock_juju_command): |
1134 | + # Legacy Juju is correctly recognized. |
1135 | + mock_juju_command.return_value = '0.7.0' |
1136 | + self.assertTrue(legacy_juju()) |
1137 | + mock_juju_command.assert_called_once_with('--version') |
1138 | + |
1139 | + def test_juju_core(self, mock_juju_command): |
1140 | + # juju-core is correctly recognized. |
1141 | + mock_juju_command.side_effect = ProcessError(1, 'failed', '', '') |
1142 | + self.assertFalse(legacy_juju()) |
1143 | + mock_juju_command.assert_called_once_with('--version') |
1144 | + |
1145 | + |
1146 | +class TestProcessError(unittest.TestCase): |
1147 | + |
1148 | + def test_str(self): |
1149 | + # The string representation of the error includes required info. |
1150 | + err = ProcessError(1, 'mycommand', 'myoutput', 'myerror') |
1151 | + expected = ( |
1152 | + "Command 'mycommand' returned non-zero exit status 1. " |
1153 | + "Output: 'myoutput'. Error: 'myerror'." |
1154 | + ) |
1155 | + self.assertEqual(expected, str(err)) |
1156 | + |
1157 | + |
1158 | +@mock.patch('time.sleep') |
1159 | +class TestRetry(unittest.TestCase): |
1160 | + |
1161 | + retry_type_error = retry(TypeError, tries=10, delay=1) |
1162 | + result = 'my value' |
1163 | + |
1164 | + def make_callable(self, side_effect): |
1165 | + mock_callable = mock.Mock() |
1166 | + mock_callable.side_effect = side_effect |
1167 | + mock_callable.__name__ = 'mock_callable' # Required by wraps. |
1168 | + decorated = retry(TypeError, tries=5, delay=1)(mock_callable) |
1169 | + return mock_callable, decorated |
1170 | + |
1171 | + def test_immediate_success(self, mock_sleep): |
1172 | + # The decorated function returns without retrying if no errors occur. |
1173 | + mock_callable, decorated = self.make_callable([self.result]) |
1174 | + result = decorated() |
1175 | + self.assertEqual(self.result, result) |
1176 | + self.assertEqual(1, mock_callable.call_count) |
1177 | + self.assertFalse(mock_sleep.called) |
1178 | + |
1179 | + def test_success(self, mock_sleep): |
1180 | + # The decorated function returns without errors after several tries. |
1181 | + side_effect = ([TypeError] * 4) + [self.result] |
1182 | + mock_callable, decorated = self.make_callable(side_effect) |
1183 | + result = decorated() |
1184 | + self.assertEqual(self.result, result) |
1185 | + self.assertEqual(5, mock_callable.call_count) |
1186 | + self.assertEqual(4, mock_sleep.call_count) |
1187 | + mock_sleep.assert_called_with(1) |
1188 | + |
1189 | + def test_failure(self, mock_sleep): |
1190 | + # The decorated function raises the last error. |
1191 | + mock_callable, decorated = self.make_callable([TypeError] * 5) |
1192 | + self.assertRaises(TypeError, decorated) |
1193 | + self.assertEqual(5, mock_callable.call_count) |
1194 | + self.assertEqual(5, mock_sleep.call_count) |
1195 | + mock_sleep.assert_called_with(1) |
1196 | + |
1197 | + |
1198 | +@mock.patch('helpers.juju_status') |
1199 | +class TestWaitForService(unittest.TestCase): |
1200 | + |
1201 | + address = 'unit.example.com' |
1202 | + service = 'test-service' |
1203 | + |
1204 | + def get_status(self, state='started', exposed=True, unit=0): |
1205 | + """Return a dict-like Juju status.""" |
1206 | + unit_name = '{}/{}'.format(self.service, unit) |
1207 | + return { |
1208 | + 'services': { |
1209 | + self.service: { |
1210 | + 'exposed': exposed, |
1211 | + 'units': { |
1212 | + unit_name: { |
1213 | + 'agent-state': state, |
1214 | + 'public-address': self.address, |
1215 | + } |
1216 | + }, |
1217 | + }, |
1218 | + }, |
1219 | + } |
1220 | + |
1221 | + def test_service_not_deployed(self, mock_juju_status): |
1222 | + # The function waits until the service is deployed. |
1223 | + mock_juju_status.side_effect = ( |
1224 | + {}, {'services': {}}, self.get_status(), |
1225 | + ) |
1226 | + unit_info = wait_for_unit(self.service) |
1227 | + self.assertEqual(self.address, unit_info['public-address']) |
1228 | + self.assertEqual(3, mock_juju_status.call_count) |
1229 | + |
1230 | + def test_service_not_exposed(self, mock_juju_status): |
1231 | + # The function waits until the service is exposed. |
1232 | + mock_juju_status.side_effect = ( |
1233 | + self.get_status(exposed=False), self.get_status(), |
1234 | + ) |
1235 | + unit_info = wait_for_unit(self.service) |
1236 | + self.assertEqual(self.address, unit_info['public-address']) |
1237 | + self.assertEqual(2, mock_juju_status.call_count) |
1238 | + |
1239 | + def test_unit_not_ready(self, mock_juju_status): |
1240 | + # The function waits until the unit is created. |
1241 | + mock_juju_status.side_effect = ( |
1242 | + {'services': {'juju-gui': {}}}, |
1243 | + {'services': {'juju-gui': {'units': {}}}}, |
1244 | + self.get_status(), |
1245 | + ) |
1246 | + unit_info = wait_for_unit(self.service) |
1247 | + self.assertEqual(self.address, unit_info['public-address']) |
1248 | + self.assertEqual(3, mock_juju_status.call_count) |
1249 | + |
1250 | + def test_state_error(self, mock_juju_status): |
1251 | + # An error is raised if the unit is in an error state. |
1252 | + mock_juju_status.return_value = self.get_status(state='install-error') |
1253 | + self.assertRaises(RuntimeError, wait_for_unit, self.service) |
1254 | + self.assertEqual(1, mock_juju_status.call_count) |
1255 | + |
1256 | + def test_not_started(self, mock_juju_status): |
1257 | + # The function waits until the unit is in a started state. |
1258 | + mock_juju_status.side_effect = ( |
1259 | + self.get_status(state='pending'), self.get_status(), |
1260 | + ) |
1261 | + unit_info = wait_for_unit(self.service) |
1262 | + self.assertEqual(self.address, unit_info['public-address']) |
1263 | + self.assertEqual(2, mock_juju_status.call_count) |
1264 | + |
1265 | + def test_unit_number(self, mock_juju_status): |
1266 | + # Different unit names are correctly handled. |
1267 | + mock_juju_status.return_value = self.get_status(unit=42) |
1268 | + unit_info = wait_for_unit(self.service) |
1269 | + self.assertEqual(self.address, unit_info['public-address']) |
1270 | + self.assertEqual(1, mock_juju_status.call_count) |
1271 | + |
1272 | + def test_public_address(self, mock_juju_status): |
1273 | + # The public address is returned when the service is ready. |
1274 | + mock_juju_status.return_value = self.get_status() |
1275 | + unit_info = wait_for_unit(self.service) |
1276 | + self.assertEqual(self.address, unit_info['public-address']) |
1277 | + self.assertEqual(1, mock_juju_status.call_count) |
1278 | |
1279 | === modified file 'tests/test_utils.py' |
1280 | --- tests/test_utils.py 2013-05-31 12:53:58 +0000 |
1281 | +++ tests/test_utils.py 2013-06-10 14:12:23 +0000 |
1282 | @@ -1,9 +1,9 @@ |
1283 | -#!/usr/bin/env python2 |
1284 | +"""Juju GUI utils tests.""" |
1285 | |
1286 | from contextlib import contextmanager |
1287 | +import json |
1288 | import os |
1289 | import shutil |
1290 | -from simplejson import dumps |
1291 | from subprocess import CalledProcessError |
1292 | import tempfile |
1293 | import unittest |
1294 | @@ -527,7 +527,7 @@ |
1295 | fd, self.log_file_name = tempfile.mkstemp() |
1296 | os.close(fd) |
1297 | mock_config = {'command-log-file': self.log_file_name} |
1298 | - charmhelpers.command = lambda *args: lambda: dumps(mock_config) |
1299 | + charmhelpers.command = lambda *args: lambda: json.dumps(mock_config) |
1300 | |
1301 | def tearDown(self): |
1302 | charmhelpers.command = self.command |
Reviewers: mp+168079_ code.launchpad. net,
Message:
Please take a look.
Description:
Replace jitsu test with juju-test.
This branch makes juju-test the default test runner
for both the unit tests and the charm functional tests.
The charm test suite now supports both pyJuju and juju-core.
The tests dependencies are now installed in a virtualenv that
is automatically created and updated by the first test
executable (00-setup). This way we don't require the user
to install all the packages, and, moreover, we make the tests
ready to be run in the charm testing infrastructure.
When the charm is deployed during tests, a temporary repository
is created where the charm is copied and then deployed.
This way we achieve two important goals:
- the developer is not forced to manually set up a local
Juju repository where to branch the Juju GUI;
- the charm can be deployed even if it contains a virtualenv, i.e.
a directory with absolute symlinks (that's because the .venv dir
is not copied in the temp repo).
The tests/helpers.py and tests/deploy.py files will be eventually
replaced by similar tools provided by the juju testing harness.
Updated the documentation where required.
Fixed unit tests exit code.
Common commands (test/lint/clean etc.) can now be easily run
using make.
Nice to have, not included in this branch:
- collect deployment test logs;
- reintroduce a way to test the charm specifying a
different juju-gui-source;
- do not require the branch to be named "juju-gui".
PS: sorry for the diff, well over 1k loc :-/
https:/ /code.launchpad .net/~frankban/ charms/ precise/ juju-gui/ use-juju- test/+merge/ 168079
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/9874050/
Affected files: functional. test nts.pip backends. py deploy. py helpers. py
M .bzrignore
M HACKING.md
A Makefile
A [revision details]
M revision
A tests/00-setup
M tests/10-unit.test
M tests/20-
A tests/deploy.py
A tests/helpers.py
A tests/requireme
M tests/test_
A tests/test_
A tests/test_
M tests/test_utils.py