Merge lp:~frankban/charms/precise/juju-gui/use-juju-test into lp:~juju-gui/charms/precise/juju-gui/trunk

Proposed by Francesco Banconi
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
Reviewer Review Type Date Requested Status
charmers Pending
Review via email: mp+168079@code.launchpad.net

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 :-/

https://codereview.appspot.com/9874050/

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

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:
   M .bzrignore
   M HACKING.md
   A Makefile
   A [revision details]
   M revision
   A tests/00-setup
   M tests/10-unit.test
   M tests/20-functional.test
   A tests/deploy.py
   A tests/helpers.py
   A tests/requirements.pip
   M tests/test_backends.py
   A tests/test_deploy.py
   A tests/test_helpers.py
   M tests/test_utils.py

Revision history for this message
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://codereview.appspot.com/9874050/diff/1/HACKING.md
File HACKING.md (right):

https://codereview.appspot.com/9874050/diff/1/HACKING.md#newcode19
HACKING.md:19: sudo apt-get install build-essential bzr libapt-pkg-dev
That's better :-)

https://codereview.appspot.com/9874050/diff/1/HACKING.md#newcode28
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://codereview.appspot.com/9874050/diff/1/HACKING.md#newcode36
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://codereview.appspot.com/9874050/diff/1/HACKING.md#newcode38
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://codereview.appspot.com/9874050/diff/1/HACKING.md#newcode42
HACKING.md:42: Before being able to run the suite, tests requirements
need to be installed
s/tests/test/

https://codereview.appspot.com/9874050/diff/1/HACKING.md#newcode96
HACKING.md:96: If you have set up your environment to run you local
development charm,
s/run you/run your/

https://codereview.appspot.com/9874050/diff/1/HACKING.md#newcode117
HACKING.md:117: created, the testing virtualenv.
Nice

https://codereview.appspot.com/9874050/diff/1/Makefile
File Makefile (right):

https://codereview.appspot.com/9874050/diff/1/Makefile#newcode1
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.

https://codereview.appspot.com/9874050/

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

LGTM, very nice work, a couple trivials. Did not QA yet, will do on
Monday.

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

https://codereview.appspot.com/9874050/diff/1/tests/20-functional.test#newcode14
tests/20-functional.test:14: ssh,
Swap the two lines above?

https://codereview.appspot.com/9874050/diff/1/tests/helpers.py
File tests/helpers.py (right):

https://codereview.appspot.com/9874050/diff/1/tests/helpers.py#newcode30
tests/helpers.py:30: be appeneded to the "baked in" arguments.
s/appeneded/appended/

https://codereview.appspot.com/9874050/diff/1/tests/helpers.py#newcode40
tests/helpers.py:40: """
Nice docstring. :-)

https://codereview.appspot.com/9874050/

Revision history for this message
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://paste.ubuntu.com/5742947/

I figure it is landable if these failures can be addressed.

https://codereview.appspot.com/9874050/diff/1/HACKING.md
File HACKING.md (right):

https://codereview.appspot.com/9874050/diff/1/HACKING.md#newcode67
HACKING.md:67: make unittest
Excellent

https://codereview.appspot.com/9874050/diff/1/HACKING.md#newcode81
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://codereview.appspot.com/9874050/diff/1/tests/helpers.py
File tests/helpers.py (right):

https://codereview.appspot.com/9874050/diff/1/tests/helpers.py#newcode66
tests/helpers.py:66: return True
This check seems very fragile. If juju-core ever grows a '--version'
option this breaks.

https://codereview.appspot.com/9874050/

77. By Francesco Banconi

Changes as per review.

78. By Francesco Banconi

s/lp/http/ in requirements.

Revision history for this message
Francesco Banconi (frankban) wrote :
Download full text (4.6 KiB)

Please take a look.

https://codereview.appspot.com/9874050/diff/1/HACKING.md
File HACKING.md (right):

https://codereview.appspot.com/9874050/diff/1/HACKING.md#newcode19
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://codereview.appspot.com/9874050/diff/1/HACKING.md#newcode36
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://codereview.appspot.com/9874050/diff/1/HACKING.md#newcode38
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://codereview.appspot.com/9874050/diff/1/HACKING.md#newcode42
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://codereview.appspot.com/9874050/diff/1/HACKING.md#newcode81
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://codereview.appspot.com/9874050/diff/1/HACKING.md#newcode96
HACKING.md:96: If you have set up your environment to...

Read more...

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

On 2013/06/07 20:05:58, bac wrote:

> Trying to run with pyjuju I get the following failures

> http://paste.ubuntu.com/5742947/

This is weird. When you are available, if you agree,
I'd like to investigate this issue with you.

https://codereview.appspot.com/9874050/

Revision history for this message
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.

https://codereview.appspot.com/9874050/

79. By Francesco Banconi

Add xvfb to the list of requirements.

Revision history for this message
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 :-/

R=
CC=
https://codereview.appspot.com/9874050

https://codereview.appspot.com/9874050/

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

Hi Gary, Nicola and Brad,
thanks for reviewing and QAing this branch!

https://codereview.appspot.com/9874050/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches