Merge lp:~vila/uci-engine/integration into lp:uci-engine

Proposed by Vincent Ladeuil
Status: Merged
Approved by: Vincent Ladeuil
Approved revision: no longer in the source branch.
Merged at revision: 430
Proposed branch: lp:~vila/uci-engine/integration
Merge into: lp:uci-engine
Diff against target: 2279 lines (+984/-641)
19 files modified
cli/tests/test_image.py (+18/-11)
juju-deployer/deploy.py (+382/-51)
juju-deployer/test_deploy.py (+243/-69)
juju-deployer/test_run.py (+68/-84)
juju-deployer/test_update.py (+14/-17)
run-tests (+67/-9)
tarmac.sh (+1/-1)
test_runner/tstrun/__init__.py (+4/-1)
test_runner/tstrun/tests/test_data_store.py (+4/-0)
tests/deployers.py (+12/-12)
tests/run.py (+49/-347)
tests/test_data_store.py (+1/-2)
tests/test_image_builder.py (+16/-3)
tests/test_integration.py (+26/-9)
tests/test_juju_gui.py (+20/-7)
tests/test_ppa_assigner.py (+23/-8)
tests/test_test_runner.py (+16/-3)
tests/test_ticket_system.py (+19/-6)
ticket_system/people/tests.py (+1/-1)
To merge this branch: bzr merge lp:~vila/uci-engine/integration
Reviewer Review Type Date Requested Status
Evan (community) Approve
PS Jenkins bot (community) continuous-integration Approve
Review via email: mp+215097@code.launchpad.net

Commit message

Complete run-tests to support all tests defined in the project (still requires a venv).

Description of the change

This refactor deploy.py and run.py to share more code so run-tests can enjoy it too.

Some renaming had to be done for the integration test files so python can import them properly (and use a cleaner namespace).

At that point, run-tests still requires a venv but that MP is big enough, I'll address that in a followup.

To post a comment you must log in.
Revision history for this message
Evan (ev) wrote :

=== modified file 'branch-source-builder/bsbuilder/run_worker.py' (properties changed: +x to -x)
=== modified file 'image-builder/imagebuilder/run_worker.py' (properties changed: +x to -x)
=== modified file 'test_runner/tstrun/run_worker.py' (properties changed: +x to -x)

:)

review: Needs Fixing
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

FAILED: Continuous integration, rev:291
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/490/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/490/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Evan (ev) wrote :

  % time ./juju-deployer/deploy.py test-runner
Traceback (most recent call last):
  File "./juju-deployer/deploy.py", line 799, in <module>
    main()
  File "./juju-deployer/deploy.py", line 770, in main
    check_and_set_ppa_privacy(args.private_ppas_only)
  File "./juju-deployer/deploy.py", line 51, in check_and_set_ppa_privacy
    current_value = os.environ['CI_PRIVATE_PPAS_ONLY']
  File "/usr/lib/python2.7/UserDict.py", line 23, in __getitem__
    raise KeyError(key)
KeyError: 'CI_PRIVATE_PPAS_ONLY'

This is because check_and_set_ppa_privacy() is called before setup() and thus check_environment() where the environment variables are set to their defaults.

review: Needs Fixing
Revision history for this message
Evan (ev) wrote :

 % time ./juju-deployer/deploy.py test-runner
Private PPAs only is DISABLED (CI_PRIVATE_PPAS_ONLY: False)
Checking juju status
Traceback (most recent call last):
  File "./juju-deployer/deploy.py", line 799, in <module>
    main()
  File "./juju-deployer/deploy.py", line 773, in main
    check_dependencies()
  File "./juju-deployer/deploy.py", line 378, in check_dependencies
    if not distutils.spawn.find_executable(cmd):
AttributeError: 'module' object has no attribute 'spawn'

92 +import distutils
...
110 -from distutils import spawn

This should be import distutils.spawn

review: Needs Fixing
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

FAILED: Continuous integration, rev:292
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/491/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/491/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Evan (ev) wrote :

  % time ./juju-deployer/deploy.py test-runner
Private PPAs only is DISABLED (CI_PRIVATE_PPAS_ONLY: False)
Checking juju status
Preparing local branch upload...
No handlers could be found for logger "keystoneclient.httpclient"
Uploading local branch, fingerprint 20b63b0bb3e58ec84ca6fb6348d163179b25f812
No revisions or tags to pull.
No revisions or tags to pull.
No revisions or tags to pull.
Traceback (most recent call last):
  File "./juju-deployer/deploy.py", line 799, in <module>
    main()
  File "./juju-deployer/deploy.py", line 780, in main
    setup()
  File "./juju-deployer/deploy.py", line 752, in setup
    os.environ['JUJU_DEPLOYER_DIR'] = working_dir
UnboundLocalError: local variable 'working_dir' referenced before assignment

Looks like you're missing an indent on:
os.environ['JUJU_DEPLOYER_DIR'] = working_dir

review: Needs Fixing
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:298
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/503/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/503/rebuild

review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:299
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/505/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/505/rebuild

review: Approve (continuous-integration)
Revision history for this message
Evan (ev) wrote :

This works a treat now. +1

review: Approve
Revision history for this message
Ubuntu CI Bot (uci-bot) wrote :
Download full text (123.5 KiB)

The attempt to merge lp:~vila/uci-engine/integration into lp:uci-engine failed. Below is the output from the failed tests.

New python executable in /tmp/tmp.DVyq0XABre/bin/python
Installing distribute.............................................................................................................................................................................................done.
Installing pip...............done.
== Testing ci-utils ....
Unpacking /tmp/tarmac/branch.Y5rJ_H/.deps/Babel-1.3.tar.gz
  Running setup.py egg_info for package from file:///tmp/tarmac/branch.Y5rJ_H/.deps/Babel-1.3.tar.gz

    warning: no previously-included files matching '*' found under directory 'docs/_build'
    warning: no previously-included files matching '*.pyc' found under directory 'tests'
    warning: no previously-included files matching '*.pyo' found under directory 'tests'
Installing collected packages: Babel
  Running setup.py install for Babel

    warning: no previously-included files matching '*' found under directory 'docs/_build'
    warning: no previously-included files matching '*.pyc' found under directory 'tests'
    warning: no previously-included files matching '*.pyo' found under directory 'tests'
    Installing pybabel script to /tmp/tmp.DVyq0XABre/bin
Successfully installed Babel
Cleaning up...
Using saved parent location: bzr+ssh://bazaar.launchpad.net/~canonical-ci-engineering/uci-engine/deps/
No revisions or tags to pull.
Unpacking /tmp/tarmac/branch.Y5rJ_H/.deps/pbr-0.6.tar.gz
  Running setup.py egg_info for package from file:///tmp/tarmac/branch.Y5rJ_H/.deps/pbr-0.6.tar.gz
    [pbr] Processing SOURCES.txt
    warning: LocalManifestMaker: standard file '-c' not found

    warning: no previously-included files found matching '.gitignore'
    warning: no previously-included files found matching '.gitreview'
    warning: no previously-included files matching '*.pyc' found anywhere in distribution
    warning: no previously-included files found matching '.gitignore'
    warning: no previously-included files found matching '.gitreview'
    warning: no previously-included files matching '*.pyc' found anywhere in distribution
Installing collected packages: pbr
  Running setup.py install for pbr
    [pbr] Reusing existing SOURCES.txt
Successfully installed pbr
Cleaning up...
Using saved parent location: bzr+ssh://bazaar.launchpad.net/~canonical-ci-engineering/uci-engine/deps/
No revisions or tags to pull.
Unpacking /tmp/tarmac/branch.Y5rJ_H/.deps/iso8601-0.1.8.tar.gz
  Running setup.py egg_info for package from file:///tmp/tarmac/branch.Y5rJ_H/.deps/iso8601-0.1.8.tar.gz

Installing collected packages: iso8601
  Running setup.py install for iso8601

Successfully installed iso8601
Cleaning up...
Using saved parent location: bzr+ssh://bazaar.launchpad.net/~canonical-ci-engineering/uci-engine/deps/
No revisions or tags to pull.
Unpacking /tmp/tarmac/branch.Y5rJ_H/.deps/prettytable-0.7.2.zip
  Running setup.py egg_info for package from file:///tmp/tarmac/branch.Y5rJ_H/.deps/prettytable-0.7.2.zip

Installing collected packages: prettytable
  Running setup.py install for prettytable

Successfully installed pret...

lp:~vila/uci-engine/integration updated
430. By Vincent Ladeuil

[r=Evan Dandrea, PS Jenkins bot] Complete run-tests to support all tests defined in the project (still requires a venv). from Vincent Ladeuil

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cli/tests/test_image.py'
2--- cli/tests/test_image.py 2014-03-17 17:17:25 +0000
3+++ cli/tests/test_image.py 2014-04-15 12:18:41 +0000
4@@ -28,7 +28,6 @@
5 from tests import capture_stdout
6
7 import urllib2
8-urllib2.urlopen = mock.Mock()
9
10
11 class FakeUrlOpenReturn:
12@@ -94,9 +93,10 @@
13 if os.path.exists(self.image_path):
14 os.remove(self.image_path)
15
16+ @mock.patch('urllib2.urlopen')
17 @mock.patch('ci_utils.data_store.create_for_ticket')
18- def test_get_ticket_image(self, mock_create_datastore):
19- urllib2.urlopen.side_effect = [
20+ def test_get_ticket_image(self, mock_create_datastore, mock_urlopen):
21+ mock_urlopen.side_effect = [
22 FakeUrlOpenReturn(200, json.dumps(artifact_data))]
23 mock_datastore = mock.Mock()
24 mock_datastore.get_file.return_value = buffer(" ")
25@@ -111,9 +111,11 @@
26 "Image file can be found at: {}\n".format(self.image_path))
27 mock_datastore.assert_called_once()
28
29+ @mock.patch('urllib2.urlopen')
30 @mock.patch('ci_utils.data_store.create_for_ticket')
31- def test_get_ticket_image_not_found(self, mock_create_datastore):
32- urllib2.urlopen.side_effect = [
33+ def test_get_ticket_image_not_found(self, mock_create_datastore,
34+ mock_urlopen):
35+ mock_urlopen.side_effect = [
36 FakeUrlOpenReturn(200, json.dumps(artifact_data))]
37 mock_datastore = mock.Mock()
38 mock_datastore.get_file.side_effect = data_store.DataStoreException(
39@@ -128,9 +130,11 @@
40 with self.assertRaises(ImageObjectNotFound):
41 args.func(args)
42
43+ @mock.patch('urllib2.urlopen')
44 @mock.patch('ci_utils.data_store.DataStore')
45- def test_get_ticket_image_already_exists(self, mock_data_store):
46- urllib2.urlopen.side_effect = [
47+ def test_get_ticket_image_already_exists(self, mock_data_store,
48+ mock_urlopen):
49+ mock_urlopen.side_effect = [
50 FakeUrlOpenReturn(200, json.dumps(artifact_data))]
51 open(self.image_path, 'a').close()
52 args = self.cli.parse_arguments(['get_image', '-t', '4', '-n',
53@@ -140,9 +144,11 @@
54 "{} already exists.\n".format(self.image_path),
55 cm)
56
57+ @mock.patch('urllib2.urlopen')
58 @mock.patch('ci_utils.data_store.create_for_ticket')
59- def test_get_last_completed_ticket_image(self, mock_create_datastore):
60- urllib2.urlopen.side_effect = [
61+ def test_get_last_completed_ticket_image(self, mock_create_datastore,
62+ mock_urlopen):
63+ mock_urlopen.side_effect = [
64 FakeUrlOpenReturn(200, json.dumps(ticket_data)),
65 FakeUrlOpenReturn(200, json.dumps(artifact_data))
66 ]
67@@ -158,9 +164,10 @@
68 "Image file can be found at: {}\n".format(self.image_path))
69 mock_datastore.assert_called_once()
70
71- def test_get_last_completed_ticket_none_complete(self):
72+ @mock.patch('urllib2.urlopen')
73+ def test_get_last_completed_ticket_none_complete(self, mock_urlopen):
74 data = {'objects': []}
75- urllib2.urlopen.side_effect = [
76+ mock_urlopen.side_effect = [
77 FakeUrlOpenReturn(200, json.dumps(data))]
78 args = self.cli.parse_arguments(['get_image', '-n', self.image_path])
79 with self.assertRaises(SystemExit) as cm:
80
81=== modified file 'juju-deployer/deploy.py'
82--- juju-deployer/deploy.py 2014-04-09 21:23:35 +0000
83+++ juju-deployer/deploy.py 2014-04-15 12:18:41 +0000
84@@ -1,19 +1,37 @@
85 #!/usr/bin/python
86+# Ubuntu Continuous Integration Engine
87+# Copyright 2013, 2014 Canonical Ltd.
88+
89+# This program is free software: you can redistribute it and/or modify it
90+# under the terms of the GNU Affero General Public License version 3, as
91+# published by the Free Software Foundation.
92+
93+# This program is distributed in the hope that it will be useful, but
94+# WITHOUT ANY WARRANTY; without even the implied warranties of
95+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
96+# PURPOSE. See the GNU Affero General Public License for more details.
97+
98+# You should have received a copy of the GNU Affero General Public License
99+# along with this program. If not, see <http://www.gnu.org/licenses/>.
100
101 import argparse
102+import atexit
103 import base64
104+from distutils import spawn
105 import hashlib
106+import itertools
107+import logging
108 import os
109+import shutil
110+import signal
111+import stat
112+import subprocess
113 import sys
114-import subprocess
115-import stat
116-import atexit
117-import shutil
118 import tempfile
119 import textwrap
120+import time
121 import yaml
122-from distutils import spawn
123-from itertools import chain
124+
125
126 import bzrlib
127 from bzrlib import (
128@@ -21,9 +39,15 @@
129 plugin,
130 workingtree,
131 )
132-
133-
134+from novaclient.v1_1 import client as nova_client
135+import novaclient.exceptions
136+import swiftclient
137+
138+
139+log = logging.getLogger(__name__)
140 HERE = os.path.abspath(os.path.dirname(__file__))
141+SSH_OPTS = ['-o', 'UserKnownHostsFile=/dev/null',
142+ '-o', 'StrictHostKeyChecking=no']
143
144 # the juju-deployer/ directory.
145 deployer_dir = os.path.abspath(os.path.dirname(sys.argv[0]))
146@@ -37,29 +61,11 @@
147 plugin.load_plugins()
148
149
150-def check_and_set_ppa_privacy(private_ppas_only):
151- current_value = os.environ['CI_PRIVATE_PPAS_ONLY']
152- if private_ppas_only:
153- value = 'True'
154- status = 'ENABLED'
155- elif current_value not in ['0', '1']:
156- print('Invalid CI_PRIVATE_PPAS_ONLY value. Options are 0 (disabled) '
157- 'and 1 (enabled). Provided value is: "{}"'.format(current_value))
158- sys.exit(1)
159- else:
160- if current_value == '0':
161- value = 'False'
162- status = 'DISABLED'
163- else:
164- value = 'True'
165- status = 'ENABLED'
166- os.environ['CI_PRIVATE_PPAS_ONLY'] = value
167- print('Private PPAs only is {} (CI_PRIVATE_PPAS_ONLY: {})'.format(status,
168- value))
169-
170-
171 def check_environment():
172- '''Cheetah requires environment variables for populating our templates'''
173+ '''Cheetah requires environment variables for populating our templates.
174+
175+ Some default values are also set here as a side-effect.
176+ '''
177 cheetah_vars = {
178 'OS_USERNAME': None,
179 'CI_CODE_SOURCE': 'branch',
180@@ -91,6 +97,258 @@
181 exit(template.format('\n '.join(missing_required)))
182
183
184+def bootstrap():
185+ """Bootstrap juju. Specify needed as bootstrap to just bootstrap, otherwise
186+ tunnel to bootstrap and tunnel."""
187+
188+ args = ['juju', 'bootstrap', '--constraints=mem=1024M']
189+ try:
190+ subprocess.check_output(args)
191+ except subprocess.CalledProcessError as e:
192+ # FIXME: if check_output raise an exception, we don't get a proper
193+ # output to check for the text below -- vila 2014-02-07
194+ if 'environment is already bootstrapped' not in e.output:
195+ raise
196+
197+
198+def _get_control_bucket():
199+ """Get the name of the bucket in Swift that juju uses for state
200+ information."""
201+
202+ default_home = os.path.join(os.path.expanduser('~'), '.juju')
203+ juju_home = os.environ.get('JUJU_HOME') or default_home
204+
205+ with open(os.path.join(juju_home, 'environments.yaml')) as fp:
206+ envs = yaml.safe_load(fp.read())
207+
208+ juju_env = os.environ.get('JUJU_ENV')
209+ if juju_env is None:
210+ juju_env = envs.get('default')
211+ if not juju_env:
212+ log.error('No juju environment specified.')
213+ sys.exit(1)
214+
215+ return envs['environments'][juju_env]['control-bucket']
216+
217+
218+def _delete_control_bucket():
219+ """Remove the juju control bucket and all of its files."""
220+
221+ bucket = _get_control_bucket()
222+ conn = _connect_to_swift()
223+ try:
224+ files = [x['name'] for x in conn.get_container(bucket)[1]]
225+ for f in files:
226+ conn.delete_object(bucket, f)
227+ conn.delete_container(bucket)
228+ except swiftclient.exceptions.ClientException:
229+ # Assume the bucket is not present
230+ pass
231+
232+
233+def _connect_to_swift():
234+ opts = {'tenant_name': os.environ['OS_TENANT_NAME'],
235+ 'region_name': os.environ['OS_REGION_NAME'],
236+ }
237+ conn = swiftclient.client.Connection(
238+ os.environ['OS_AUTH_URL'],
239+ os.environ['OS_USERNAME'],
240+ os.environ['OS_PASSWORD'],
241+ os_options=opts,
242+ auth_version='2.0')
243+ return conn
244+
245+
246+def _get_from_swift(bucket, path):
247+ """Download the file path from swift at bucket."""
248+
249+ conn = _connect_to_swift()
250+ # get_object return a tuple of (metadata, data)
251+ return conn.get_object(bucket, path)[1]
252+
253+
254+def get_bootstrap_ip(instance, max_tries=120):
255+ """Wait for the juju bootstrap node to boot and obtain an IP address.
256+
257+ This is a pre-requisite to setup a tunnel.
258+
259+ :raises: NotFound if the instance has not been spawned.
260+
261+ :raises: Exception if no IP can be found after max_tries*3 seconds.
262+ """
263+
264+ log.info("Getting the bootstrap IP address.")
265+
266+ args = [os.environ['OS_USERNAME'], os.environ['OS_PASSWORD'],
267+ os.environ['OS_TENANT_NAME'], os.environ['OS_AUTH_URL']]
268+ kwargs = {'region_name': os.environ['OS_REGION_NAME'],
269+ 'service_type': 'compute'}
270+ nt = nova_client.Client(*args, **kwargs)
271+
272+ tries = 0
273+ while tries < max_tries:
274+ # Raises NotFound if the instance does not exist.
275+ server = nt.servers.get(instance)
276+ networks = server.networks.values()
277+ if networks:
278+ # We use a single network and don't care how it's named
279+ net = networks[0]
280+ if net:
281+ # Either we have a single address or the last one has been
282+ # added to make the instance reachable (floating or public).
283+ net = net[-1]
284+ # Don't sleep anymore. We have our IP, let's move on
285+ return net
286+
287+ tries += 1
288+ if tries != max_tries:
289+ time.sleep(3)
290+ raise Exception('Timed out waiting for bootstrap IP.')
291+
292+
293+def get_bootstrap_nova_instance():
294+ """Get the Nova instance identifier for the juju bootstrap node.
295+
296+ Raises swiftclient.exceptions.ClientException if the instance cannot be
297+ found."""
298+
299+ bucket = _get_control_bucket()
300+
301+ state = yaml.safe_load(_get_from_swift(bucket, 'provider-state'))
302+ if not state:
303+ # Cause unknown (yet) but this has been seen in real life. I can't
304+ # reproduce anymore but I suspect 'juju bootstrap' failed mid-way so
305+ # returning None and letting the caller delete the bucket is the Right
306+ # Thing to do -- vila 2014-02-07
307+ log.warning("Empty 'provider-state' in the juju bucket")
308+ return None
309+ return state['state-instances'][0]
310+
311+
312+def wait_for_node(ip):
313+ """Wait for the bootstrap node to become available on ssh. We need to wait
314+ for this state before attempting to set up a sshuttle."""
315+
316+ log.info("Waiting for the bootstrap node to respond to ssh.")
317+
318+ cmd = ['ssh'] + SSH_OPTS + ['ubuntu@%s' % ip, 'exit']
319+ tries = 0
320+ while tries < 120:
321+ try:
322+ subprocess.check_call(cmd)
323+ return
324+ except subprocess.CalledProcessError:
325+ tries += 1
326+ time.sleep(5)
327+
328+ raise Exception('Timed out waiting for bootstrap node.')
329+
330+
331+def destroy_tunnel():
332+ if os.path.exists('sshuttle.pid'):
333+ with open('sshuttle.pid') as fp:
334+ pid = int(fp.read())
335+ os.kill(pid, signal.SIGTERM)
336+
337+
338+def tunnel(ip):
339+ """Tunnel to the bootstrap node using sshuttle."""
340+
341+ log.info("Tunneling to the bootstrap node.")
342+
343+ host = 'ubuntu@%s' % ip
344+ mask = '10.55.0.0/16'
345+ cmd = ['sshuttle', '-D', '-r', host, mask]
346+ opt = ['-e', 'ssh %s' % ' '.join(SSH_OPTS)]
347+ subprocess.check_call(cmd + opt)
348+ atexit.register(destroy_tunnel)
349+
350+
351+def wait_then_tunnel():
352+ instance = get_bootstrap_nova_instance()
353+ ip = get_bootstrap_ip(instance)
354+ wait_for_node(ip)
355+ tunnel(ip)
356+
357+
358+def needs_bootstrap():
359+ """Check to see if there's a Swift bucket for juju. If not, we need to
360+ boostrap. Check to see if the IP of the bootstrap node is owned by a
361+ running Nova instance. If not, we need to bootstrap."""
362+
363+ log.info("Checking to see if a bootstrap is needed.")
364+
365+ def inconsistent_state(msg):
366+ log.warning(msg)
367+ _delete_control_bucket()
368+
369+ try:
370+ instance = get_bootstrap_nova_instance()
371+ except swiftclient.exceptions.ClientException:
372+ instance = None
373+ if instance is None:
374+ # We don't have an instance. We need to bootstrap.
375+ inconsistent_state('No bootstrap instance in the swift bucket')
376+ return True
377+
378+ try:
379+ get_bootstrap_ip(instance, 1)
380+ except novaclient.exceptions.NotFound:
381+ # Juju claims we have an instance that does not exist. We're in an
382+ # inconsistent state. Delete the bucket and bootstrap.
383+ inconsistent_state('No IP for the bootstrap instance')
384+ return True
385+
386+ return False
387+
388+
389+def _get_pid_of_sshuttle(ip_addr):
390+ try:
391+ cmd = ['pgrep', '-f', 'sshuttle.*%s' % ip_addr]
392+ pid = subprocess.check_output(cmd)
393+ # pgrep returns a list of pids matching the search argument
394+ pid.split('\n')[0]
395+ return int(pid)
396+ except subprocess.CalledProcessError:
397+ return -1
398+
399+
400+def private_IP(addr):
401+ """Check whether an IP address can be routed.
402+ :param addr: The IP address as a string.
403+
404+ :returns: True if the address is private (can't be routed). False
405+ otherwise.
406+
407+ """
408+ if addr.startswith('10.'): # 10.0.0.0 - 10.255.255.255
409+ return True
410+ elif addr.startswith('192.168'): # 192.168.0.0 - 192.168.255.255
411+ return True
412+ elif addr.startswith('172.'): # 172.16.0.0 - 172.31.255.255
413+ a, b, c, d = addr.split('.')
414+ if 16 <= int(b) <= 31:
415+ return True
416+ return False
417+
418+
419+def needs_tunnel():
420+ """Check to see if a tunnel is required and up.
421+
422+ When required, it depends on juju's Swift bucket existing, and the IP it
423+ refers to is pointed at by a sshuttle instance.
424+ """
425+ log.info("Checking to see if a tunnel needs to be setup.")
426+
427+ instance = get_bootstrap_nova_instance()
428+ ip_addr = get_bootstrap_ip(instance, 1)
429+ if private_IP(ip_addr):
430+ sshuttle_pid = _get_pid_of_sshuttle(ip_addr)
431+ return sshuttle_pid < 0
432+ else:
433+ return False
434+
435+
436 def check_bootstrapped():
437 try:
438 # XXX: Added a print to give user feedback in case sshuttle isn't
439@@ -116,6 +374,20 @@
440 sys.exit(1)
441
442
443+def populate_all_templates():
444+ deployer_dir = os.path.join(HERE, '..', 'juju-deployer')
445+ temp = tempfile.mkdtemp()
446+ atexit.register(lambda: shutil.rmtree(temp))
447+ # A place to put our generated .yaml files.
448+ working_dir = os.path.join(temp, 'deployer')
449+ # Copy all existing .yaml files, they don't need to be populated but
450+ # juju-deployer expect them in 'working_dir'.
451+ shutil.copytree(deployer_dir, working_dir)
452+ # Create .yaml files from .yaml.tmpl files.
453+ populate_templates(deployer_dir, working_dir)
454+ return working_dir
455+
456+
457 def populate_templates(deployer_dir, working_dir):
458 # The command to populate the template.
459 cmd_base = ['cheetah', 'fill', '--env', '-p']
460@@ -123,8 +395,12 @@
461 for tmpl in os.listdir(deployer_dir):
462 path = os.path.join(deployer_dir, tmpl)
463 if os.path.isdir(path):
464+ # When called with a directory, recurse and process all templates
465+ # found there.
466 src = os.path.join(deployer_dir, tmpl)
467 dst = os.path.join(working_dir, tmpl)
468+ if not os.path.isdir(dst):
469+ os.mkdir(dst)
470 populate_templates(src, dst)
471 continue
472 if not tmpl.endswith('.yaml.tmpl'):
473@@ -178,6 +454,31 @@
474 cmd.run_argv_aliases(args)
475
476
477+def install_charmworld(where=None):
478+ """Recent versions of amulet need charmworldlib. Install it."""
479+ if where is None:
480+ where = os.path.join(HERE, '..', 'branches')
481+ dest = os.path.abspath(os.path.join(where, 'charmworldlib'))
482+ get_or_pull_branch('lp:charmworldlib', dest)
483+ prepend_path('PYTHONPATH', dest)
484+ # FIXME: This is a symptom that our setup story needs to receive some more
485+ # love -- vila 2014-04-10
486+ sys.path.insert(0, dest)
487+ return dest
488+
489+
490+def install_amulet(where=None):
491+ if where is None:
492+ where = os.path.join(HERE, '..', 'branches')
493+ dest = os.path.abspath(os.path.join(where, 'amulet'))
494+ get_or_pull_branch('lp:amulet', dest)
495+ prepend_path('PYTHONPATH', dest)
496+ # FIXME: This is a symptom that our setup story needs to receive some more
497+ # love -- vila 2014-04-10
498+ sys.path.insert(0, dest)
499+ return dest
500+
501+
502 def install_websocket_client(where=None):
503 """Install python-websocket-client from bzr, as jujuclient needs it."""
504 if where is None:
505@@ -205,9 +506,10 @@
506 if where is None:
507 where = os.path.join(HERE, '..', 'branches')
508 dest = os.path.abspath(os.path.join(where, 'juju-deployer'))
509- # For an obscure reason tag:0.3.1 is not in lp:juju-deployer
510 get_or_pull_branch('lp:juju-deployer', dest, revision='revno:103')
511 prepend_path('PYTHONPATH', dest)
512+ # FIXME: This is a symptom that our setup story needs to receive some more
513+ # love -- vila 2014-04-10
514 sys.path.insert(0, dest)
515 # FIXME: This duplicates setup.py and shouldn't be needed. This specific
516 # piece (juju-deployer) is a weird beast, it installs from source (not pip
517@@ -415,45 +717,74 @@
518 return parser.parse_args(args)
519
520
521+def setup(for_tests=False, list_only=False):
522+ """Setup the environment for a deployment.
523+
524+ This includes:
525+ - installing the source dependencies,
526+ - uploading our source payload to swift,
527+ - populating the templates,
528+ - starting the juju boootstrap node if needed, with a tunnel if needed.
529+ """
530+ if for_tests:
531+ install_amulet()
532+ install_charmworld()
533+ install_websocket_client()
534+ install_jujuclient()
535+ install_deployer()
536+
537+ check_environment()
538+ if for_tests:
539+ if not list_only:
540+ upload_payload_to_swift()
541+ working_dir = populate_all_templates()
542+ if not list_only:
543+ build_charms()
544+ # FIXME: We use JUJU_DEPLOYER_DIR to tell deployers.DeployerTest where
545+ # the populated templates are, there should be a better way than using
546+ # an env var as a global variable -- vila 2014-04-11
547+ os.environ['JUJU_DEPLOYER_DIR'] = working_dir
548+ if for_tests:
549+ if needs_bootstrap():
550+ bootstrap()
551+ if needs_tunnel():
552+ wait_then_tunnel()
553+
554+
555 def main():
556 args = _get_args()
557
558- check_environment()
559-
560 # You can configure the system to use private PPAs only or by providing
561 # the option --private-ppas-only or by setting the environment variable
562 # CI_PRIVATE_PPAS_ONLY.
563 # If provided, --private-ppas-only option overrides the environment
564 # variable value.
565- check_and_set_ppa_privacy(args.private_ppas_only)
566+ if args.private_ppas_only:
567+ os.environ['CI_PRIVATE_PPAS_ONLY'] = '1'
568
569 check_bootstrapped()
570 check_dependencies()
571
572- install_websocket_client()
573- install_jujuclient()
574- install_deployer()
575-
576- # A place to put our generated .yaml files.
577- temp_dir = tempfile.mkdtemp()
578- atexit.register(shutil.rmtree, temp_dir)
579- working_dir = os.path.join(temp_dir, 'deployer')
580- # Copy all existing .yaml files, they don't need to be populated but
581- # juju-deployer expect them in 'working_dir'.
582- shutil.copytree(deployer_dir, working_dir)
583-
584 if not args.branch_payload and not args.reuse_payload:
585+ # We set some env variables below so this should happen before
586+ # populating the templates
587 upload_payload_to_swift()
588
589- # Create .yaml files from .yaml.tmpl files.
590- populate_templates(deployer_dir, working_dir)
591+ setup()
592+ priv = os.environ['CI_PRIVATE_PPAS_ONLY']
593+ if priv == '0':
594+ status = 'DISABLED'
595+ else:
596+ status = 'ENABLED'
597+ print('Private PPAs only is {} (CI_PRIVATE_PPAS_ONLY: {})'.format(status,
598+ priv))
599+ working_dir = populate_all_templates()
600+ build_charms()
601
602 # Generate an argument list of -c FILE for each .yaml file in our working
603 # directory.
604 i = generate_argument_list(working_dir, args.specific)
605- generated = list(chain.from_iterable(i))
606-
607- build_charms()
608+ generated = list(itertools.chain.from_iterable(i))
609
610 if args.upgrade:
611 for x in generated:
612
613=== modified file 'juju-deployer/test_deploy.py'
614--- juju-deployer/test_deploy.py 2014-03-27 13:40:30 +0000
615+++ juju-deployer/test_deploy.py 2014-04-15 12:18:41 +0000
616@@ -1,14 +1,36 @@
617 #!/usr/bin/env python
618-
619+# Ubuntu CI Engine
620+# Copyright 2014 Canonical Ltd.
621+
622+# This program is free software: you can redistribute it and/or modify it
623+# under the terms of the GNU Affero General Public License version 3, as
624+# published by the Free Software Foundation.
625+
626+# This program is distributed in the hope that it will be useful, but
627+# WITHOUT ANY WARRANTY; without even the implied warranties of
628+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
629+# PURPOSE. See the GNU Affero General Public License for more details.
630+
631+# You should have received a copy of the GNU Affero General Public License
632+# along with this program. If not, see <http://www.gnu.org/licenses/>.
633+
634+from cStringIO import StringIO
635 import mock
636-import unittest
637 import os
638 import sys
639 import tempfile
640 import shutil
641 import subprocess
642-
643-
644+import unittest
645+
646+from novaclient import exceptions
647+import swiftclient
648+
649+from bzrlib import (
650+ branchbuilder,
651+ errors,
652+)
653+from bzrlib.transport import memory
654 from ucitests import fixtures
655
656
657@@ -16,13 +38,24 @@
658 import deploy
659
660
661-class TestDeploy(unittest.TestCase):
662+class TestCheckEnvironment(unittest.TestCase):
663+
664+ def setUp(self):
665+ super(TestCheckEnvironment, self).setUp()
666+ fixtures.set_uniq_cwd(self)
667
668 def test_check_environment(self):
669- # If we haven't sourced a novarc, this program should bail out.
670- with mock.patch.dict('os.environ', {'OS_USERNAME': ''}):
671- with self.assertRaises(SystemExit):
672- deploy.check_environment()
673+ # If we haven't sourced a valid novarc, this program should bail out.
674+ fixtures.isolate_from_env(self, dict(OS_USERNAME=''))
675+ with self.assertRaises(SystemExit):
676+ deploy.check_environment()
677+
678+
679+class TestCheckDependencies(unittest.TestCase):
680+
681+ def setUp(self):
682+ super(TestCheckDependencies, self).setUp()
683+ fixtures.set_uniq_cwd(self)
684
685 # check that some of our values can be defaulted correctly:
686 env_required = {
687@@ -33,20 +66,82 @@
688 'CI_LAUNCHPAD_USER': 'lp-user',
689 'CI_LAUNCHPAD_PPA_OWNER': 'lp-ppa-owner',
690 }
691- with mock.patch.dict('os.environ', env_required):
692- deploy.check_environment()
693- for key, val in env_required.iteritems():
694- self.assertEqual(val, os.environ[key])
695- # defaulted by code
696- self.assertEqual(os.environ['CI_CODE_SOURCE'], 'branch')
697- self.assertEqual(os.environ['CI_PRIVATE_PPAS_ONLY'], '0')
698+ fixtures.isolate_from_env(self, env_required)
699+ deploy.check_environment()
700+ for key, val in env_required.iteritems():
701+ self.assertEqual(val, os.environ[key])
702+ # defaulted by code
703+ self.assertEqual(os.environ['CI_CODE_SOURCE'], 'branch')
704+ self.assertEqual(os.environ['CI_PRIVATE_PPAS_ONLY'], '0')
705
706 def test_check_dependencies(self):
707 # If we cannot find any of our dependencies, this program should bail
708 # out.
709- with mock.patch.dict('os.environ', {'PATH': ''}):
710- with self.assertRaises(SystemExit):
711- deploy.check_dependencies()
712+ fixtures.isolate_from_env(self, dict(PATH=''))
713+ with self.assertRaises(SystemExit):
714+ deploy.check_dependencies()
715+
716+
717+class TestGetOrPullBranch(unittest.TestCase):
718+
719+ def setUp(self):
720+ super(TestGetOrPullBranch, self).setUp()
721+ fixtures.set_uniq_cwd(self)
722+ fixtures.override_env(self, 'BZR_EMAIL', 'foo@example.com')
723+ builder = branchbuilder.BranchBuilder(
724+ memory.MemoryTransport("memory:///"))
725+ builder.start_series()
726+ builder.build_snapshot(
727+ 'rev-id', None,
728+ [('add', ('', 'root-id', 'directory', '')),
729+ ('add', ('filename', 'f-id', 'file', 'content\n'))])
730+ builder.build_snapshot('rev2-id', ['rev-id'],
731+ [('modify', ('f-id', 'new-content\n'))])
732+ builder.finish_series()
733+ # The tested API want local paths, let's create the branch on disk
734+ tree = builder.get_branch().create_checkout('the_branch')
735+ self.branch = tree.branch
736+ # Capture output
737+ self.out = StringIO()
738+ self.err = StringIO()
739+ # silence progress report
740+ fixtures.override_env(self, 'BZR_PROGRESS_BAR', 'none')
741+
742+ def do_branch(self, *args):
743+ deploy.get_or_pull_branch(*args, out=self.out, err=self.err)
744+ self.assertEqual('', self.err.getvalue())
745+
746+ def test_get_branch(self):
747+ self.do_branch('the_branch', 'here')
748+ self.assertTrue(os.path.exists('here'))
749+ self.assertEqual('', self.out.getvalue())
750+
751+ def test_pull_branch(self):
752+ self.do_branch('the_branch', 'here')
753+ self.do_branch('the_branch', 'here', 'revid:rev-id')
754+ self.assertEqual('Now on revision 1.\n', self.out.getvalue())
755+ with open(os.path.join('here', 'filename')) as f:
756+ self.assertEqual('content\n', f.read())
757+
758+ def assertErrorPath(self, dir_name, exc):
759+ expected = os.path.join(self.uniq_dir, dir_name) + '/'
760+ self.assertEqual(expected, exc.path)
761+
762+ def test_branch_bogus_src(self):
763+ with self.assertRaises(errors.NotBranchError) as cm:
764+ self.do_branch('I-dont-exist', 'here')
765+ self.assertErrorPath('I-dont-exist', cm.exception)
766+ self.assertEqual('', self.out.getvalue())
767+
768+ def test_pull_bogus_src(self):
769+ deploy.get_or_pull_branch('the_branch', 'here')
770+ with self.assertRaises(errors.NotBranchError) as cm:
771+ self.do_branch('I-dont-exist', 'here')
772+ self.assertErrorPath('I-dont-exist', cm.exception)
773+ self.assertEqual('', self.out.getvalue())
774+
775+
776+class TestBootStrap(unittest.TestCase):
777
778 def test_check_not_bootstrapped(self):
779 # If juju environment isn't bootstrapped, this program should bail
780@@ -62,75 +157,95 @@
781 # If juju status returns ok, this should just work.
782 deploy.check_bootstrapped()
783
784+
785+class TestInstallDeployer(unittest.TestCase):
786+
787+ def setUp(self):
788+ super(TestInstallDeployer, self).setUp()
789+ fixtures.set_uniq_cwd(self)
790+
791 def test_install_deployer(self):
792+ fixtures.isolate_from_env(self, dict(PYTHONPATH=''))
793+ deployer_dir = deploy.install_deployer('.')
794+ self.addCleanup(shutil.rmtree, deployer_dir)
795+ self.assertTrue(os.path.exists(os.path.join(deployer_dir, '.bzr')))
796+ self.assertEqual(deployer_dir, os.environ['PYTHONPATH'])
797+ deployer_script = os.path.join(deployer_dir, 'juju-deployer')
798+ self.assertTrue(os.access(deployer_script, os.X_OK))
799+ self.assertTrue(os.path.exists(deployer_script))
800+
801+
802+class TestGenerateArgumentList(unittest.TestCase):
803+
804+ def setUp(self):
805+ super(TestGenerateArgumentList, self).setUp()
806 fixtures.set_uniq_cwd(self)
807- with mock.patch.dict('os.environ', {'PYTHONPATH': ''}):
808- deployer_dir = deploy.install_deployer('.')
809- self.assertTrue(os.path.exists(os.path.join(deployer_dir, '.bzr')))
810- self.assertEqual(deployer_dir, os.environ['PYTHONPATH'])
811- deployer_script = os.path.join(deployer_dir, 'juju-deployer')
812- self.assertTrue(os.access(deployer_script, os.X_OK))
813- self.assertTrue(os.path.exists(deployer_script))
814
815 def test_generate_empty_list(self):
816- working = tempfile.mkdtemp()
817- self.addCleanup(lambda: shutil.rmtree(working))
818-
819 # Calling without anything in the directory should produce an empty
820 # list.
821- self.assertEqual(list(deploy.generate_argument_list(working)), [])
822+ self.assertEqual([], list(deploy.generate_argument_list('.')))
823
824 def test_generate_explicit_list(self):
825- working = tempfile.mkdtemp()
826- self.addCleanup(lambda: shutil.rmtree(working))
827-
828- expected = os.path.join(working, 'foo.yaml')
829+ expected = os.path.join('.', 'foo.yaml')
830 with open(expected, 'w') as fp:
831 fp.write('nothing')
832 # Calling with a basic configuration should produce the full path to
833 # that configuration in the form ('-c', path).
834- gen = deploy.generate_argument_list(working)
835+ gen = deploy.generate_argument_list('.')
836 self.assertEqual([('-c', expected)], list(gen))
837
838+
839+class TestPopulateTemplates(unittest.TestCase):
840+
841+ def setUp(self):
842+ super(TestPopulateTemplates, self).setUp()
843+ fixtures.set_uniq_cwd(self)
844+ self.target = os.path.join('.', 'target')
845+ os.mkdir(self.target)
846+ self.source = os.path.join('.', 'source')
847+ os.mkdir(self.source)
848+
849 def test_populate_templates_empty_list(self):
850- fake_deployer_dir = tempfile.mkdtemp()
851- self.addCleanup(lambda: shutil.rmtree(fake_deployer_dir))
852- working = tempfile.mkdtemp()
853- self.addCleanup(lambda: shutil.rmtree(working))
854-
855- deploy.populate_templates(fake_deployer_dir, working)
856+ deploy.populate_templates(self.source, self.target)
857 # Nothing happened
858- self.assertEqual([], os.listdir(working))
859-
860- @mock.patch.dict('os.environ', {'USER': 'foo'})
861- def test_populate_templates_explicit_list(self, *args):
862- fake_deployer_dir = tempfile.mkdtemp()
863- self.addCleanup(lambda: shutil.rmtree(fake_deployer_dir))
864- working = tempfile.mkdtemp()
865- self.addCleanup(lambda: shutil.rmtree(working))
866-
867- test_files = ['config.yaml', 'configs/unit_config.yaml']
868- for f in test_files:
869- src = os.path.join(fake_deployer_dir, f)
870- dst = os.path.join(working, f)
871- if not os.path.exists(os.path.dirname(src)):
872- os.mkdir(os.path.dirname(src))
873- if not os.path.exists(os.path.dirname(dst)):
874- os.mkdir(os.path.dirname(dst))
875-
876- with open(src + '.tmpl', 'w') as f:
877- f.write('me: ${USER}')
878-
879- deploy.populate_templates(fake_deployer_dir, working)
880-
881- for f in test_files:
882- f = os.path.join(working, f)
883- content = open(f).read()
884- self.assertEqual('me: {}'.format(os.environ['USER']), content)
885+ self.assertEqual([], os.listdir(self.source))
886+
887+ def test_populate_templates_explicit_list(self):
888+ fixtures.override_env(self, 'USER', 'foo')
889+ stem = 'config.yaml'
890+ with open(os.path.join(self.source, stem + '.tmpl'), 'w') as f:
891+ f.write('me: ${USER}')
892+ deploy.populate_templates(self.source, self.target)
893+ with open(os.path.join(self.target, stem)) as f:
894+ content = f.read()
895+ self.assertEqual('me: {}'.format(os.environ['USER']), content)
896+
897+ def test_populate_recurse(self):
898+ fixtures.override_env(self, 'USER', 'foo')
899+ configs_dir = os.path.join(self.source, 'configs')
900+ os.mkdir(configs_dir)
901+ stem = 'config.yaml'
902+ with open(os.path.join(configs_dir, stem + '.tmpl'), 'w') as f:
903+ f.write('me: ${USER}')
904+ # The only existing template is one level below self.source so
905+ # populate_templates should recurse there.
906+ deploy.populate_templates(self.source, self.target)
907+ # Accordingly, the results should one level below self.target
908+ with open(os.path.join(self.target, 'configs', stem)) as f:
909+ content = f.read()
910+ self.assertEqual('me: {}'.format(os.environ['USER']), content)
911+
912+
913+class TestBuildCharms(unittest.TestCase):
914+
915+ def setUp(self):
916+ super(TestBuildCharms, self).setUp()
917+ fixtures.set_uniq_cwd(self)
918
919 def test_build_charms(self):
920 charmsdir = tempfile.mkdtemp()
921- self.addCleanup(lambda: shutil.rmtree(charmsdir))
922+ self.addCleanup(shutil.rmtree, charmsdir)
923 p = os.path.join(charmsdir, 'precise')
924 os.mkdir(p)
925 charm1 = os.path.join(p, 'charm1')
926@@ -151,5 +266,64 @@
927 self.assertItemsEqual(['charm1', 'charm2', 'webui'], os.listdir(p))
928
929
930+class TestPrivateIP(unittest.TestCase):
931+
932+ def test_public(self):
933+ self.assertFalse(deploy.private_IP('2.2.2.2'))
934+
935+ def test_class_A_private(self):
936+ self.assertTrue(deploy.private_IP('10.0.4.5'))
937+
938+ def test_class_B_public(self):
939+ self.assertFalse(deploy.private_IP('172.15.2.245'))
940+
941+ def test_class_B_private(self):
942+ self.assertTrue(deploy.private_IP('172.24.12.36'))
943+
944+ def test_class_C_private(self):
945+ self.assertTrue(deploy.private_IP('192.168.0.16'))
946+
947+
948+class TestInstallAmulet(unittest.TestCase):
949+
950+ def setUp(self):
951+ super(TestInstallAmulet, self).setUp()
952+ fixtures.set_uniq_cwd(self)
953+
954+ def test_install_amulet(self):
955+ fixtures.override_env(self, 'PYTHONPATH', '')
956+ deploy.install_amulet('.')
957+ self.assertTrue(os.path.exists(os.path.join('amulet', '.bzr')))
958+ self.assertEqual(os.path.join(self.uniq_dir, 'amulet'),
959+ os.environ['PYTHONPATH'])
960+
961+
962+class TestNeedsBotstrap(unittest.TestCase):
963+
964+ @mock.patch('deploy._get_control_bucket',
965+ return_value='juju-d911b43e427c204cfcfd8c7276c4b2aa')
966+ def test_needs_bootstrap(self, mock_get_control_bucket):
967+ provider_state = '{ state-instances: ["abcdefg"] }'
968+ with mock.patch('deploy._get_from_swift', return_value=provider_state):
969+ # Bootstrap needed - the bucket exists but the instance does not.
970+ e = exceptions.NotFound('Four oh four')
971+ with mock.patch('deploy.get_bootstrap_ip', side_effect=e):
972+ with mock.patch('deploy._delete_control_bucket') as dcb:
973+ self.assertTrue(deploy.needs_bootstrap())
974+ dcb.assert_called_with()
975+
976+ # Bootstrap not needed.
977+ ip_addr = '127.0.0.1'
978+ with mock.patch('deploy.get_bootstrap_ip', return_value=ip_addr):
979+ self.assertFalse(deploy.needs_bootstrap())
980+
981+ # Bootstrap needed - the bucket does not exist.
982+ e = swiftclient.exceptions.ClientException('not found')
983+ with mock.patch('deploy._get_from_swift', side_effect=e):
984+ with mock.patch('deploy._delete_control_bucket') as dcb:
985+ self.assertTrue(deploy.needs_bootstrap())
986+ dcb.assert_called_with()
987+
988+
989 if __name__ == '__main__':
990 unittest.main()
991
992=== renamed file 'tests/test_run.py' => 'juju-deployer/test_run.py'
993--- tests/test_run.py 2014-04-09 14:54:06 +0000
994+++ juju-deployer/test_run.py 2014-04-15 12:18:41 +0000
995@@ -1,26 +1,42 @@
996 #!/usr/bin/env python
997-
998-import unittest
999+# Ubuntu CI Engine
1000+# Copyright 2014 Canonical Ltd.
1001+
1002+# This program is free software: you can redistribute it and/or modify it
1003+# under the terms of the GNU Affero General Public License version 3, as
1004+# published by the Free Software Foundation.
1005+
1006+# This program is distributed in the hope that it will be useful, but
1007+# WITHOUT ANY WARRANTY; without even the implied warranties of
1008+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1009+# PURPOSE. See the GNU Affero General Public License for more details.
1010+
1011+# You should have received a copy of the GNU Affero General Public License
1012+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1013+
1014+import logging
1015 import mock
1016-import run
1017-import yaml
1018-import tempfile
1019-import shutil
1020 import os
1021-import logging
1022 import stat
1023-import swiftclient
1024-from novaclient.exceptions import NotFound
1025+import sys
1026+import tempfile
1027+import unittest
1028+import yaml
1029
1030
1031 from ucitests import fixtures
1032
1033-
1034-# Disable all (almost) log output from the tests and the code they run
1035+# FIXME: Disable logging while running tests, this will be revisited once we
1036+# have a proper way to capture log in tests. -- vila 2014-02-12
1037 logging.disable(logging.CRITICAL)
1038
1039-
1040-class TestRunTests(unittest.TestCase):
1041+mydir = os.path.dirname(__file__)
1042+sys.path.append(os.path.abspath(os.path.join( mydir, '..', 'tests')))
1043+import run
1044+
1045+
1046+class TestDestroyServices(unittest.TestCase):
1047+
1048 def _destroy_services(self, fake_status, destroy_machine, destroy_service):
1049 fake_status = yaml.safe_dump(fake_status)
1050
1051@@ -43,60 +59,47 @@
1052 fake_status = {'services': {}, 'machines': {}}
1053 self._destroy_services(fake_status, destroy_machine, destroy_service)
1054
1055- def test_get_tests_no_tests(self):
1056- temp_dir = tempfile.mkdtemp()
1057- self.addCleanup(lambda: shutil.rmtree(temp_dir))
1058- self.assertEqual(run.get_tests(temp_dir), [])
1059+
1060+class TestGetTests(unittest.TestCase):
1061+ """Minimally test get_tests until run-tests can replace ./tests/run.py.
1062+
1063+ We test against the real source but it's ok as we're read-only.
1064+ """
1065+
1066+ def setUp(self):
1067+ super(TestGetTests, self).setUp()
1068+ fixtures.set_uniq_cwd(self)
1069+
1070+ def create_file(self, name, content=''):
1071+ # We only care about files existing
1072+ with open(name, 'w') as f:
1073+ f.write(content)
1074+
1075+ def test_no_tests(self):
1076+ self.assertEqual([], run.get_tests('.'))
1077+
1078+ def test_matches_from_env(self):
1079+ fixtures.isolate_from_env(self, dict(TESTS='foo'))
1080+ self.create_file('test_foo.py')
1081+ self.assertEqual(['./test_foo.py'], run.get_tests('.'))
1082+
1083+ def test_filtered_from_env(self):
1084+ fixtures.isolate_from_env(self, dict(TESTS='foo'))
1085+ self.create_file('test_foo.py')
1086+ self.create_file('test_bar.py')
1087+ self.assertEqual(['./test_foo.py'], run.get_tests('.'))
1088
1089 def test_get_tests(self):
1090- temp_dir = tempfile.mkdtemp()
1091- self.addCleanup(lambda: shutil.rmtree(temp_dir))
1092- os.mkdir(os.path.join(temp_dir, 'foo'))
1093- os.mkdir(os.path.join(temp_dir, 'bar'))
1094- expect = [os.path.join(temp_dir, x, 'test.py') for x in ('foo', 'bar')]
1095-
1096- for x in expect:
1097- with open(x, 'w') as fp:
1098- fp.write('#!/usr/bin/python\nprint "hello"\n')
1099- with open(os.path.join(temp_dir, 'README'), 'w') as fp:
1100- fp.write('Empty.')
1101- os.mkdir(os.path.join(temp_dir, 'baz'))
1102- with open(os.path.join(temp_dir, 'baz', 'README'), 'w') as fp:
1103- fp.write('Empty.')
1104-
1105- self.assertEqual(sorted(run.get_tests(temp_dir)), sorted(expect))
1106-
1107- def test_install_amulet(self):
1108- fixtures.set_uniq_cwd(self)
1109- with mock.patch.dict('os.environ', {'PYTHONPATH': ''}):
1110- amulet_dir = run.install_amulet('.')
1111- self.addCleanup(shutil.rmtree, amulet_dir)
1112- self.assertTrue(os.path.exists(os.path.join(amulet_dir, '.bzr')))
1113- self.assertEqual(amulet_dir, os.environ['PYTHONPATH'])
1114-
1115- @mock.patch('run._get_control_bucket',
1116- return_value='juju-d911b43e427c204cfcfd8c7276c4b2aa')
1117- def test_needs_bootstrap(self, mock_get_control_bucket):
1118- provider_state = '{ state-instances: ["abcdefg"] }'
1119- with mock.patch('run._get_from_swift', return_value=provider_state):
1120- # Bootstrap needed - the bucket exists but the instance does not.
1121- e = NotFound('Four oh four')
1122- with mock.patch('run.get_bootstrap_ip', side_effect=e):
1123- with mock.patch('run._delete_control_bucket') as dcb:
1124- self.assertTrue(run.needs_bootstrap())
1125- dcb.assert_called_with()
1126-
1127- # Bootstrap not needed.
1128- ip_addr = '127.0.0.1'
1129- with mock.patch('run.get_bootstrap_ip', return_value=ip_addr):
1130- self.assertFalse(run.needs_bootstrap())
1131-
1132- # Bootstrap needed - the bucket does not exist.
1133- e = swiftclient.exceptions.ClientException('not found')
1134- with mock.patch('run._get_from_swift', side_effect=e):
1135- with mock.patch('run._delete_control_bucket') as dcb:
1136- self.assertTrue(run.needs_bootstrap())
1137- dcb.assert_called_with()
1138+ expected = [os.path.join('.', 'test_{}.py'.format(x))
1139+ for x in ('foo', 'bar')]
1140+
1141+ for x in expected:
1142+ self.create_file(x, '#!/usr/bin/python\nprint "hello"\n')
1143+ self.create_file('README')
1144+ self.assertEqual(sorted(expected), sorted(run.get_tests('.')))
1145+
1146+
1147+class TestRunTests(unittest.TestCase):
1148
1149 def test_check_sudoers(self):
1150 with tempfile.NamedTemporaryFile() as fp:
1151@@ -139,24 +142,5 @@
1152 self.assertEqual(result, ['ci-juju-gui/0'])
1153
1154
1155-class TestPrivateIP(unittest.TestCase):
1156-
1157- def test_public(self):
1158- self.assertFalse(run.private_IP('2.2.2.2'))
1159-
1160- def test_class_A_private(self):
1161- self.assertTrue(run.private_IP('10.0.4.5'))
1162-
1163- def test_class_B_public(self):
1164- self.assertFalse(run.private_IP('172.15.2.245'))
1165-
1166- def test_class_B_private(self):
1167- self.assertTrue(run.private_IP('172.24.12.36'))
1168-
1169- def test_class_C_private(self):
1170- self.assertTrue(run.private_IP('192.168.0.16'))
1171-
1172-
1173-
1174 if __name__ == '__main__':
1175 unittest.main()
1176
1177=== modified file 'juju-deployer/test_update.py'
1178--- juju-deployer/test_update.py 2014-02-19 14:48:22 +0000
1179+++ juju-deployer/test_update.py 2014-04-15 12:18:41 +0000
1180@@ -60,28 +60,26 @@
1181 class TestGetDeployerConfigs(unittest.TestCase):
1182
1183 def setUp(self):
1184- self.temp_dir = tempfile.mkdtemp()
1185- self.addCleanup(lambda: shutil.rmtree(self.temp_dir))
1186+ super(TestGetDeployerConfigs, self).setUp()
1187+ fixtures.set_uniq_cwd(self)
1188
1189 def test_get_deployer_no_configs(self):
1190- configs = list(update.get_deployer_configs(self.temp_dir))
1191+ configs = list(update.get_deployer_configs('.'))
1192 self.assertEqual(configs, [])
1193
1194 def test_get_deployer_two_configs(self):
1195 for x in ('foo.yaml', 'bar.yaml.tmpl'):
1196- with open(os.path.join(self.temp_dir, x), 'w') as fp:
1197+ with open(x, 'w') as fp:
1198 fp.write('nothing: here')
1199- foo = os.path.join(self.temp_dir, 'foo.yaml')
1200- bar = os.path.join(self.temp_dir, 'bar.yaml.tmpl')
1201- self.assertEqual(sorted([foo, bar]),
1202- sorted(update.get_deployer_configs(self.temp_dir)))
1203+ self.assertEqual(sorted(['./bar.yaml.tmpl', './foo.yaml']),
1204+ sorted(update.get_deployer_configs('.')))
1205
1206
1207 class TestUpdate(unittest.TestCase):
1208
1209 def setUp(self):
1210- self.temp_dir = tempfile.mkdtemp()
1211- self.addCleanup(lambda: shutil.rmtree(self.temp_dir))
1212+ super(TestUpdate, self).setUp()
1213+ fixtures.set_uniq_cwd(self)
1214 fixtures.override_env(self, 'BZR_EMAIL', 'foo@example.com')
1215 # We need a real branch with at least 2 revisions (so we can check out
1216 # of date references)
1217@@ -97,8 +95,7 @@
1218 builder.finish_series()
1219 # get_branches_and_revnos won't be able to use a in-memory branch,
1220 # let's create one on disk.
1221- dest = os.path.join(self.temp_dir, 'the_branch')
1222- tree = builder.get_branch().create_checkout(dest)
1223+ tree = builder.get_branch().create_checkout('the_branch')
1224 self.branch = tree.branch
1225
1226 def get_branch_url(self):
1227@@ -120,14 +117,14 @@
1228
1229 def test_ensure_all_branches_are_pinned(self):
1230 yaml_string = self.get_service_yaml(1)
1231- path = os.path.join(self.temp_dir, 'foo.yaml')
1232+ path = 'foo.yaml'
1233 with open(path, 'w') as fp:
1234 fp.write(yaml.safe_dump(yaml_string))
1235 self.assertTrue(update.ensure_all_branches_are_pinned([path]))
1236
1237 def test_ensure_some_branches_are_not_pinned(self):
1238 yaml_string = self.get_service_yaml()
1239- path = os.path.join(self.temp_dir, 'foo.yaml')
1240+ path = 'foo.yaml'
1241 with open(path, 'w') as fp:
1242 fp.write(yaml.safe_dump(yaml_string))
1243 self.assertFalse(update.ensure_all_branches_are_pinned([path]))
1244@@ -136,7 +133,7 @@
1245 branch_url = self.get_branch_url()
1246 current = self.branch.revno()
1247 y = self.get_service_yaml(current - 1)
1248- path = os.path.join(self.temp_dir, 'foo.yaml')
1249+ path = 'foo.yaml'
1250 with open(path, 'w') as fp:
1251 fp.write(yaml.safe_dump(y))
1252
1253@@ -151,7 +148,7 @@
1254 def test_set_branches_and_revnos_on_naked_branches(self):
1255 branch_url = self.get_branch_url()
1256 y = self.get_service_yaml()
1257- path = os.path.join(self.temp_dir, 'foo.yaml')
1258+ path = 'foo.yaml'
1259 with open(path, 'w') as fp:
1260 fp.write(yaml.safe_dump(y))
1261 update.set_branches_and_revnos([path], {branch_url: 1})
1262@@ -164,7 +161,7 @@
1263 def test_set_branches_and_revnos_on_pinned_branches(self):
1264 branch_url = self.get_branch_url()
1265 y = self.get_service_yaml(1)
1266- path = os.path.join(self.temp_dir, 'foo.yaml')
1267+ path = 'foo.yaml'
1268 with open(path, 'w') as fp:
1269 fp.write(yaml.safe_dump(y))
1270 update.set_branches_and_revnos([path], {branch_url: 2})
1271
1272=== modified file 'run-tests'
1273--- run-tests 2014-03-27 13:40:30 +0000
1274+++ run-tests 2014-04-15 12:18:41 +0000
1275@@ -22,6 +22,11 @@
1276 import sys
1277
1278
1279+HERE = os.path.abspath(os.path.dirname(__file__))
1280+
1281+# deploy.py isn't in our pythonpath when run as a script
1282+sys.path.append(os.path.join(HERE, 'juju-deployer'))
1283+import deploy
1284 from ucitests import (
1285 filters,
1286 loaders,
1287@@ -243,8 +248,46 @@
1288 return failures
1289
1290
1291-def run_regular_components(suite, out, result, options):
1292- """Run tests for all the regular components.
1293+def load_orphan_tests(include_regexps, exclude_regexps=None):
1294+ """Load tests matching inclusive and exclusive regexps.
1295+
1296+ :param include_regexps: A list of regexps describing the tests to include.
1297+
1298+ :param exclude_regexps: A list of regexps describing the tests to exclude.
1299+
1300+ :return: The test suite for all collected tests.
1301+ """
1302+ components = ['juju-deployer']
1303+ loader = loaders.Loader()
1304+ suite = loader.suiteClass()
1305+ for c in components:
1306+ suite.addTests(load_component_tests(loader, c))
1307+ suite = filters.include_regexps(include_regexps, suite)
1308+ suite = filters.exclude_regexps(exclude_regexps, suite)
1309+ return suite
1310+
1311+
1312+def load_integration_tests(include_regexps, exclude_regexps=None):
1313+ """Load tests matching inclusive and exclusive regexps.
1314+
1315+ :param include_regexps: A list of regexps describing the tests to include.
1316+
1317+ :param exclude_regexps: A list of regexps describing the tests to exclude.
1318+
1319+ :return: The test suite for all collected tests.
1320+ """
1321+ components = ['tests']
1322+ loader = loaders.Loader()
1323+ suite = loader.suiteClass()
1324+ for c in components:
1325+ suite.addTests(load_component_tests(loader, c))
1326+ suite = filters.include_regexps(include_regexps, suite)
1327+ suite = filters.exclude_regexps(exclude_regexps, suite)
1328+ return suite
1329+
1330+
1331+def run_regular_tests(suite, out, result, options):
1332+ """Run the given test suite under some isolation.
1333
1334 :param suite: The test suite to run.
1335
1336@@ -289,16 +332,31 @@
1337 suite = load_regular_component_tests(options.include_regexps,
1338 options.exclude_regexps)
1339 # Regular components
1340- ret = run_regular_components(suite, stdout, result, options)
1341+ ret = run_regular_tests(suite, stdout, result, options)
1342+ # orphan tests that don't have a proper home yet
1343+ suite = load_orphan_tests(options.include_regexps,
1344+ options.exclude_regexps)
1345+ orphan_rc = run_regular_tests(suite, stdout, result, options)
1346+ ret = orphan_rc or ret
1347 # Django-based components
1348 with IsolatedImportAndLogging():
1349- ppa = run_django_tests('ppa-assigner', 'ppa_assigner', True,
1350- stdout, result, options)
1351- ret = ppa or ret
1352+ ppa_rc = run_django_tests('ppa-assigner', 'ppa_assigner', True,
1353+ stdout, result, options)
1354+ ret = ppa_rc or ret
1355 with IsolatedImportAndLogging():
1356- ts = run_django_tests('ticket_system', 'ticket_system', True,
1357- stdout, result, options)
1358- ret = ts or ret
1359+ ts_rc = run_django_tests('ticket_system', 'ticket_system', True,
1360+ stdout, result, options)
1361+ ret = ts_rc or ret
1362+ # integration tests
1363+ with IsolatedImportAndLogging(), SysPath(['ci-utils']):
1364+ # FIXME: We need some more setup here that tests themselves should do
1365+ # but they aren't ripe for that yet -- vila 2014-04-11
1366+ deploy.setup(for_tests=True, list_only=options.list_only)
1367+ suite = load_integration_tests(options.include_regexps,
1368+ options.exclude_regexps)
1369+ itg_rc = run_regular_tests(suite, stdout, result, options)
1370+ ret = itg_rc or ret
1371+
1372 if not options.list_only:
1373 # We did run tests, stop the run
1374 result.stopTestRun()
1375
1376=== modified file 'tarmac.sh'
1377--- tarmac.sh 2014-03-26 15:35:02 +0000
1378+++ tarmac.sh 2014-04-15 12:18:41 +0000
1379@@ -58,7 +58,7 @@
1380 ./juju-deployer/update.py --assert-pinned
1381 ./juju-deployer/test_update.py
1382 ./juju-deployer/test_deploy.py
1383-./tests/test_run.py
1384+./juju-deployer/test_run.py
1385 '
1386 # Do not want to parse newlines as the internal field separator. It would break
1387 # any test with arguments.
1388
1389=== modified file 'test_runner/tstrun/__init__.py'
1390--- test_runner/tstrun/__init__.py 2014-04-10 18:03:39 +0000
1391+++ test_runner/tstrun/__init__.py 2014-04-15 12:18:41 +0000
1392@@ -20,11 +20,14 @@
1393 HERE = os.path.abspath(os.path.dirname(__file__))
1394
1395
1396+HERE = os.path.abspath(os.path.dirname(__file__))
1397+
1398+
1399 # Creating the nova instance for the testbed and storing test results in the
1400 # data store require nova credentials.
1401 def get_auth_config(path=None):
1402 if path is None:
1403- path = os.path.abspath(os.path.join(HERE, '../../unit_config'))
1404+ path = os.path.abspath(os.path.join(HERE, '..', '..', 'unit_config'))
1405 with open(path) as f:
1406 config = yaml.safe_load(f)
1407 return config
1408
1409=== modified file 'test_runner/tstrun/tests/test_data_store.py'
1410--- test_runner/tstrun/tests/test_data_store.py 2014-02-17 10:13:29 +0000
1411+++ test_runner/tstrun/tests/test_data_store.py 2014-04-15 12:18:41 +0000
1412@@ -103,6 +103,10 @@
1413 # Get our content back
1414 self.assertEqual(content, self.store.get_file(rel_path))
1415
1416+ def test_create_container_twice(self):
1417+ # swift is perfectly happy to create the same container twice.
1418+ self.store._create_container(self.store.container)
1419+
1420
1421 if __name__ == '__main__':
1422 unittest.main()
1423
1424=== removed directory 'tests/data_store'
1425=== modified file 'tests/deployers.py'
1426--- tests/deployers.py 2014-03-10 22:25:00 +0000
1427+++ tests/deployers.py 2014-04-15 12:18:41 +0000
1428@@ -15,16 +15,15 @@
1429
1430 import base64
1431 import os
1432-import shutil
1433 import subprocess
1434 import unittest
1435
1436+
1437+import amulet
1438 import yaml
1439
1440-import amulet
1441
1442-defdir = os.path.join(os.path.dirname(__file__), '../juju-deployer')
1443-DEPLOYER_DIR = os.environ.get('JUJU_DEPLOYER_DIR', defdir)
1444+HERE = os.path.dirname(__file__)
1445
1446
1447 class AmuletDeployment(object):
1448@@ -57,6 +56,9 @@
1449 val = base64.b64encode(f.read())
1450 service['options'][key] = val
1451
1452+ def tearDown(self):
1453+ self.deployment.cleanup()
1454+
1455
1456 class DeployerTest(unittest.TestCase):
1457 '''Base class for building juju deployer based tests.'''
1458@@ -66,19 +68,17 @@
1459
1460 def setUp(self):
1461 super(DeployerTest, self).setUp()
1462- self.deployer = AmuletDeployment(self.deployer_cfg)
1463- self.addCleanup(self._clean_amulet)
1464- if self.timeout:
1465- self.deployer.setUp(self.timeout)
1466+ self.deployer = AmuletDeployment(
1467+ os.path.join(os.environ['JUJU_DEPLOYER_DIR'], self.deployer_cfg))
1468+ self.addCleanup(self.deployer.tearDown)
1469+ self.deployer.setUp(self.timeout)
1470 self.status = amulet.waiter.status(self.deployer.deployment.juju_env)
1471
1472- def _clean_amulet(self):
1473- if self.deployer.deployment.deployer_dir:
1474- shutil.rmtree(self.deployer.deployment.deployer_dir)
1475-
1476 def get_ip_and_port(self, service, unit=0):
1477 units = self.status['services'][service]['units']
1478 ip = units['%s/%d' % (service, unit)]['public-address']
1479+ # FIXME: Can fail with KeyError: 'open-ports'. Need to refresh status ?
1480+ # -- vila 2014-04-10
1481 port, _ = units['%s/%d' % (service, unit)]['open-ports'][0].split('/')
1482 return ip, int(port)
1483
1484
1485=== removed directory 'tests/image_builder'
1486=== removed directory 'tests/integration'
1487=== removed directory 'tests/juju_gui'
1488=== removed directory 'tests/ppa_assigner'
1489=== modified file 'tests/run.py'
1490--- tests/run.py 2014-04-10 14:55:27 +0000
1491+++ tests/run.py 2014-04-15 12:18:41 +0000
1492@@ -1,23 +1,28 @@
1493 #!/usr/bin/python
1494+# Ubuntu CI Engine
1495+# Copyright 2014 Canonical Ltd.
1496+
1497+# This program is free software: you can redistribute it and/or modify it
1498+# under the terms of the GNU Affero General Public License version 3, as
1499+# published by the Free Software Foundation.
1500+
1501+# This program is distributed in the hope that it will be useful, but
1502+# WITHOUT ANY WARRANTY; without even the implied warranties of
1503+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1504+# PURPOSE. See the GNU Affero General Public License for more details.
1505+
1506+# You should have received a copy of the GNU Affero General Public License
1507+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1508
1509 import atexit
1510 import logging
1511 import os
1512-import shutil
1513-import signal
1514 import subprocess
1515 import sys
1516-import tempfile
1517 import time
1518 import yaml
1519
1520-import swiftclient
1521-
1522-from novaclient.v1_1 import client
1523-from novaclient.exceptions import NotFound
1524-
1525-SSH_OPTS = ['-o', 'UserKnownHostsFile=/dev/null',
1526- '-o', 'StrictHostKeyChecking=no']
1527+
1528 SUDOERS_FILE = '/etc/sudoers.d/sshuttle'
1529 SUDOERS = 'ALL = (root) NOPASSWD: /usr/bin/python /usr/lib/sshuttle/main.py *'
1530
1531@@ -28,9 +33,8 @@
1532
1533 HERE = os.path.abspath(os.path.dirname(__file__))
1534
1535-
1536-# deploy.py probably isn't in our pythonpath
1537-sys.path.append(os.path.join(os.path.dirname(__file__), '../juju-deployer'))
1538+# deploy.py isn't in our pythonpath when run as a script
1539+sys.path.append(os.path.join(HERE, '../juju-deployer'))
1540 import deploy
1541
1542
1543@@ -86,6 +90,9 @@
1544 """Assert that there are no deployed services or nodes beyond the bootstrap
1545 node."""
1546
1547+ # FIXME: This can block on ci-juju-gui in an error state (seen in real
1548+ # life). -- vila 2014-02-14
1549+
1550 # First ensure that anything dying has made the transition to the other
1551 # side.
1552 _wait_for_death()
1553@@ -117,33 +124,30 @@
1554 _destroy_service(service)
1555
1556
1557-def _find_test_files(test_dir, tests):
1558- for test in tests:
1559- actual_test = os.path.join(test_dir, test, 'test.py')
1560- if not os.path.exists(actual_test):
1561- msg = "Skipping %s as %s does not exist." % (test, actual_test)
1562- log.error(msg)
1563- else:
1564- yield actual_test
1565-
1566-
1567-def _find_dirs_in_dir(test_dir):
1568- """Return an iterator of only the directories in test_dir."""
1569-
1570- for x in os.listdir(test_dir):
1571- if os.path.isdir(os.path.join(test_dir, x)):
1572- yield x
1573-
1574-
1575 def get_tests(test_dir):
1576 """Returns a list of full relative paths to test files."""
1577-
1578- tests = os.environ.get('TESTS', '')
1579- if tests:
1580- tests = tests.split(' ')
1581- else:
1582- tests = _find_dirs_in_dir(test_dir)
1583- return list(_find_test_files(test_dir, tests))
1584+ tests = []
1585+ for name in os.listdir(test_dir):
1586+ if name.startswith('test_') and name.endswith('.py'):
1587+ tests.append(name)
1588+ only = os.environ.get('TESTS', '')
1589+ if only:
1590+ selected = []
1591+ only = only.split()
1592+ # TESTS specifies which components should be tested, for compatibility
1593+ # with the previous layout where a 'test.py' file was under a directory
1594+ # named like the component, we keep matching on that.
1595+ for one in only:
1596+ file_name = 'test_{}.py'.format(one)
1597+ if file_name in tests:
1598+ selected.append(file_name)
1599+ else:
1600+ msg = "Skipping {} as {} does not exist in {}"
1601+ log.error(msg.format(one, file_name, test_dir))
1602+ tests = selected
1603+ # Prepend the test dir to all selected tests
1604+ tests = [os.path.join(test_dir, name) for name in tests]
1605+ return tests
1606
1607
1608 def find_failed_units():
1609@@ -166,285 +170,6 @@
1610 subprocess.check_call(cmd)
1611
1612
1613-def install_charmworld(where=None):
1614- """Recent versions of amulet need charmworldlib. Install it."""
1615- if where is None:
1616- where = os.path.join(HERE, '..', 'branches')
1617- dest = os.path.abspath(os.path.join(where, 'charmworldlib'))
1618- deploy.get_or_pull_branch('lp:charmworldlib', dest)
1619- deploy.prepend_path('PYTHONPATH', dest)
1620- return dest
1621-
1622-
1623-def install_amulet(where=None):
1624- if where is None:
1625- where = os.path.join(HERE, '..', 'branches')
1626- dest = os.path.abspath(os.path.join(where, 'amulet'))
1627- deploy.get_or_pull_branch('lp:amulet', dest)
1628- deploy.prepend_path('PYTHONPATH', dest)
1629- return dest
1630-
1631-
1632-def remove_branch(branch_dir):
1633- """Clean up the bzr branch if we didn't already do so as a result of
1634- bzr failing."""
1635-
1636- if os.path.exists(branch_dir):
1637- shutil.rmtree(branch_dir)
1638-
1639-
1640-def needs_bootstrap():
1641- """Check to see if there's a Swift bucket for juju. If not, we need to
1642- boostrap. Check to see if the IP of the bootstrap node is owned by a
1643- running Nova instance. If not, we need to bootstrap."""
1644-
1645- log.info("Checking to see if a bootstrap is needed.")
1646-
1647- def inconsistent_state(msg):
1648- log.warning(msg)
1649- _delete_control_bucket()
1650-
1651- try:
1652- instance = get_bootstrap_nova_instance()
1653- except swiftclient.exceptions.ClientException:
1654- instance = None
1655- if instance is None:
1656- # We don't have an instance. We need to bootstrap.
1657- inconsistent_state('No bootstrap instance in the swift bucket')
1658- return True
1659-
1660- try:
1661- get_bootstrap_ip(instance, 1)
1662- except NotFound:
1663- # Juju claims we have an instance that does not exist. We're in an
1664- # inconsistent state. Delete the bucket and bootstrap.
1665- inconsistent_state('No IP for the bootstrap instance')
1666- return True
1667-
1668- return False
1669-
1670-
1671-def _get_pid_of_sshuttle(ip_addr):
1672- try:
1673- cmd = ['pgrep', '-f', 'sshuttle.*%s' % ip_addr]
1674- pid = subprocess.check_output(cmd)
1675- # pgrep returns a list of pids matching the search argument
1676- pid.split('\n')[0]
1677- return int(pid)
1678- except subprocess.CalledProcessError:
1679- return -1
1680-
1681-
1682-def needs_tunnel():
1683- """Check to see if a tunnel is required and up.
1684-
1685- When required, it depends on juju's Swift bucket existing, and the IP it
1686- refers to is pointed at by a sshuttle instance.
1687- """
1688- log.info("Checking to see if a tunnel needs to be setup.")
1689-
1690- instance = get_bootstrap_nova_instance()
1691- ip_addr = get_bootstrap_ip(instance, 1)
1692- if private_IP(ip_addr):
1693- sshuttle_pid = _get_pid_of_sshuttle(ip_addr)
1694- return sshuttle_pid < 0
1695- else:
1696- return False
1697-
1698-
1699-def _connect_to_swift():
1700- opts = {'tenant_name': os.environ['OS_TENANT_NAME'],
1701- 'region_name': os.environ['OS_REGION_NAME'],
1702- }
1703- conn = swiftclient.client.Connection(
1704- os.environ['OS_AUTH_URL'],
1705- os.environ['OS_USERNAME'],
1706- os.environ['OS_PASSWORD'],
1707- os_options=opts,
1708- auth_version='2.0')
1709- return conn
1710-
1711-
1712-def _get_from_swift(bucket, path):
1713- """Download the file path from swift at bucket."""
1714-
1715- conn = _connect_to_swift()
1716- # get_object return a tuple of (metadata, data)
1717- return conn.get_object(bucket, path)[1]
1718-
1719-
1720-def _get_control_bucket():
1721- """Get the name of the bucket in Swift that juju uses for state
1722- information."""
1723-
1724- default_home = os.path.join(os.path.expanduser('~'), '.juju')
1725- juju_home = os.environ.get('JUJU_HOME') or default_home
1726-
1727- with open(os.path.join(juju_home, 'environments.yaml')) as fp:
1728- envs = yaml.safe_load(fp.read())
1729-
1730- juju_env = os.environ.get('JUJU_ENV')
1731- if juju_env is None:
1732- juju_env = envs.get('default')
1733- if not juju_env:
1734- log.error('No juju environment specified.')
1735- sys.exit(1)
1736-
1737- return envs['environments'][juju_env]['control-bucket']
1738-
1739-
1740-def _delete_control_bucket():
1741- """Remove the juju control bucket and all of its files."""
1742-
1743- bucket = _get_control_bucket()
1744- conn = _connect_to_swift()
1745- try:
1746- files = [x['name'] for x in conn.get_container(bucket)[1]]
1747- for f in files:
1748- conn.delete_object(bucket, f)
1749- conn.delete_container(bucket)
1750- except swiftclient.exceptions.ClientException:
1751- # Assume the bucket is not present
1752- pass
1753-
1754-
1755-def get_bootstrap_nova_instance():
1756- """Get the Nova instance identifier for the juju bootstrap node.
1757-
1758- Raises swiftclient.exceptions.ClientException if the instance cannot be
1759- found."""
1760-
1761- bucket = _get_control_bucket()
1762-
1763- state = yaml.safe_load(_get_from_swift(bucket, 'provider-state'))
1764- if not state:
1765- # Cause unknown (yet) but this has been seen in real life. I can't
1766- # reproduce anymore but I suspect 'juju bootstrap' failed mid-way so
1767- # returning None and letting the caller delete the bucket is the Right
1768- # Thing to do -- vila 2014-02-07
1769- log.warning("Empty 'provider-state' in the juju bucket")
1770- return None
1771- return state['state-instances'][0]
1772-
1773-
1774-def private_IP(addr):
1775- """Check whether an IP address can be routed.
1776- :param addr: The IP address as a string.
1777-
1778- :returns: True if the address is private (can't be routed). False
1779- otherwise.
1780-
1781- """
1782- if addr.startswith('10.'): # 10.0.0.0 - 10.255.255.255
1783- return True
1784- elif addr.startswith('192.168'): # 192.168.0.0 - 192.168.255.255
1785- return True
1786- elif addr.startswith('172.'): # 172.16.0.0 - 172.31.255.255
1787- a, b, c, d = addr.split('.')
1788- if 16 <= int(b) <= 31:
1789- return True
1790- return False
1791-
1792-
1793-def get_bootstrap_ip(instance, max_tries=120):
1794- """Wait for the juju bootstrap node to boot and obtain an IP address.
1795-
1796- This is a pre-requisite to setup a tunnel.
1797-
1798- :raises: NotFound if the instance has not been spawned.
1799-
1800- :raises: Exception if no IP can be found after max_tries*3 seconds.
1801- """
1802-
1803- log.info("Getting the bootstrap IP address.")
1804-
1805- args = [os.environ['OS_USERNAME'], os.environ['OS_PASSWORD'],
1806- os.environ['OS_TENANT_NAME'], os.environ['OS_AUTH_URL']]
1807- kwargs = {'region_name': os.environ['OS_REGION_NAME'],
1808- 'service_type': 'compute'}
1809- nt = client.Client(*args, **kwargs)
1810-
1811- tries = 0
1812- while tries < max_tries:
1813- # Raises NotFound if the instance does not exist.
1814- server = nt.servers.get(instance)
1815- networks = server.networks.values()
1816- if networks:
1817- # We use a single network and don't care how it's named
1818- net = networks[0]
1819- if net:
1820- # Either we have a single address or the last one has been
1821- # added to make the instance reachable (floating or public).
1822- net = net[-1]
1823- # Don't sleep anymore. We have our IP, let's move on
1824- return net
1825-
1826- tries += 1
1827- if tries != max_tries:
1828- time.sleep(3)
1829- raise Exception('Timed out waiting for bootstrap IP.')
1830-
1831-
1832-def destroy_tunnel():
1833- if os.path.exists('sshuttle.pid'):
1834- with open('sshuttle.pid') as fp:
1835- pid = int(fp.read())
1836- os.kill(pid, signal.SIGTERM)
1837-
1838-
1839-def tunnel(ip):
1840- """Tunnel to the bootstrap node using sshuttle."""
1841-
1842- log.info("Tunneling to the bootstrap node.")
1843-
1844- host = 'ubuntu@%s' % ip
1845- mask = '10.55.0.0/16'
1846- cmd = ['sshuttle', '-D', '-r', host, mask]
1847- opt = ['-e', 'ssh %s' % ' '.join(SSH_OPTS)]
1848- subprocess.check_call(cmd + opt)
1849- atexit.register(destroy_tunnel)
1850-
1851-
1852-def wait_for_node(ip):
1853- """Wait for the bootstrap node to become available on ssh. We need to wait
1854- for this state before attempting to set up a sshuttle."""
1855-
1856- log.info("Waiting for the bootstrap node to respond to ssh.")
1857-
1858- cmd = ['ssh'] + SSH_OPTS + ['ubuntu@%s' % ip, 'exit']
1859- tries = 0
1860- while tries < 120:
1861- try:
1862- subprocess.check_call(cmd)
1863- return
1864- except subprocess.CalledProcessError:
1865- tries += 1
1866- time.sleep(5)
1867-
1868- raise Exception('Timed out waiting for bootstrap node.')
1869-
1870-
1871-def bootstrap():
1872- """Bootstrap juju. Specify needed as bootstrap to just bootstrap, otherwise
1873- tunnel to bootstrap and tunnel."""
1874-
1875- args = ['juju', 'bootstrap', '--constraints=mem=1024M']
1876- try:
1877- subprocess.check_output(args)
1878- except subprocess.CalledProcessError as e:
1879- # FIXME: if check_output raise an exception, we don't get a proper
1880- # output to check for the text below -- vila 2014-02-07
1881- if 'environment is already bootstrapped' not in e.output:
1882- raise
1883-
1884-
1885-def wait_then_tunnel():
1886- instance = get_bootstrap_nova_instance()
1887- ip = get_bootstrap_ip(instance)
1888- wait_for_node(ip)
1889- tunnel(ip)
1890-
1891-
1892 def check_sudoers(sudoers_path=SUDOERS_FILE):
1893 """Check to see if /etc/sudoers.d/sshuttle exists and is of the right
1894 form."""
1895@@ -492,23 +217,10 @@
1896 return rc == 0
1897
1898
1899-def _prepare_deployer():
1900- deployer_dir = os.path.join(os.path.dirname(__file__), '../juju-deployer')
1901- temp = tempfile.mkdtemp()
1902- atexit.register(lambda: shutil.rmtree(temp))
1903- working_dir = os.path.join(temp, 'deployer')
1904- shutil.copytree(deployer_dir, working_dir)
1905- deploy.upload_payload_to_swift()
1906- deploy.populate_templates(deployer_dir, working_dir)
1907- deploy.build_charms()
1908- os.environ['JUJU_DEPLOYER_DIR'] = working_dir
1909-
1910-
1911 def main():
1912 check_nova_env()
1913
1914 test_dir = os.path.dirname(sys.argv[0])
1915- deploy.check_environment()
1916 tests = get_tests(test_dir)
1917 if not tests:
1918 # Running no tests is an error
1919@@ -524,32 +236,22 @@
1920 log.error(msg)
1921 sys.exit(1)
1922
1923- _prepare_deployer()
1924+ deploy.setup(for_tests=True)
1925
1926- install_amulet()
1927- install_charmworld()
1928- deploy.install_deployer()
1929- deploy.install_jujuclient()
1930+ destroy_services()
1931+ assert_no_deployed_services()
1932+ atexit.register(destroy_services)
1933
1934 # Now, insert the 'tests' dir in front of PYTHONPATH so we get our tests
1935 # without colliding with other 'tests' directories defined by other
1936 # packages (including our deps).
1937 deploy.prepend_path('PYTHONPATH', os.path.dirname(test_dir))
1938
1939- if needs_bootstrap():
1940- bootstrap()
1941- if needs_tunnel():
1942- wait_then_tunnel()
1943-
1944- destroy_services()
1945- assert_no_deployed_services()
1946- atexit.register(destroy_services)
1947-
1948 returncode = 0
1949 for test in tests:
1950- log.error('========================================')
1951- log.error('Testing %s' % test)
1952- log.error('========================================')
1953+ log.info('========================================')
1954+ log.info('Testing %s' % test)
1955+ log.info('========================================')
1956 try:
1957 subprocess.check_call(['python', test])
1958 except subprocess.CalledProcessError as e:
1959
1960=== renamed file 'tests/data_store/test.py' => 'tests/test_data_store.py'
1961--- tests/data_store/test.py 2014-02-17 10:13:29 +0000
1962+++ tests/test_data_store.py 2014-04-15 12:18:41 +0000
1963@@ -20,8 +20,7 @@
1964 import sys
1965 import yaml
1966
1967-sys.path.append(os.path.join(
1968- os.path.dirname(__file__), '..', '..', 'ci-utils'))
1969+sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'ci-utils'))
1970
1971 from ci_utils import data_store
1972
1973
1974=== renamed file 'tests/image_builder/test.py' => 'tests/test_image_builder.py'
1975--- tests/image_builder/test.py 2014-04-02 11:56:54 +0000
1976+++ tests/test_image_builder.py 2014-04-15 12:18:41 +0000
1977@@ -1,18 +1,31 @@
1978 #!/usr/bin/env python
1979+# Ubuntu Continuous Integration Engine
1980+# Copyright 2014 Canonical Ltd.
1981+
1982+# This program is free software: you can redistribute it and/or modify it
1983+# under the terms of the GNU Affero General Public License version 3, as
1984+# published by the Free Software Foundation.
1985+
1986+# This program is distributed in the hope that it will be useful, but
1987+# WITHOUT ANY WARRANTY; without even the implied warranties of
1988+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1989+# PURPOSE. See the GNU Affero General Public License for more details.
1990+
1991+# You should have received a copy of the GNU Affero General Public License
1992+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1993
1994 import json
1995-import os
1996 import unittest
1997
1998 import httplib2
1999
2000-from tests import deployers
2001+import deployers
2002
2003
2004 class TestTestRunner(deployers.DeployerTest):
2005 """Integration tests for image builder service run on a juju deployment"""
2006
2007- deployer_cfg = os.path.join(deployers.DEPLOYER_DIR, 'image-builder.yaml')
2008+ deployer_cfg = 'image-builder.yaml'
2009
2010 def get_status(self):
2011 url = 'http://{}:{}/api/v1/status'.format(
2012
2013=== renamed file 'tests/integration/test.py' => 'tests/test_integration.py'
2014--- tests/integration/test.py 2014-03-07 13:23:35 +0000
2015+++ tests/test_integration.py 2014-04-15 12:18:41 +0000
2016@@ -1,4 +1,18 @@
2017 #!/usr/bin/env python
2018+# Ubuntu Continuous Integration Engine
2019+# Copyright 2014 Canonical Ltd.
2020+
2021+# This program is free software: you can redistribute it and/or modify it
2022+# under the terms of the GNU Affero General Public License version 3, as
2023+# published by the Free Software Foundation.
2024+
2025+# This program is distributed in the hope that it will be useful, but
2026+# WITHOUT ANY WARRANTY; without even the implied warranties of
2027+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2028+# PURPOSE. See the GNU Affero General Public License for more details.
2029+
2030+# You should have received a copy of the GNU Affero General Public License
2031+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2032
2033 import os
2034 import json
2035@@ -9,7 +23,10 @@
2036
2037 import httplib2
2038
2039-from tests.deployers import DEPLOYER_DIR, DeployerTest
2040+import deployers
2041+
2042+
2043+HERE = os.path.abspath(os.path.dirname(__file__))
2044
2045
2046 def create_virtual_env():
2047@@ -24,10 +41,9 @@
2048
2049 def run_virtual_env(temp_dir, args, cwd=None):
2050 if args is 'create':
2051- cwd = os.path.join(os.path.dirname(__file__), '../../cli')
2052- changes = os.path.abspath(os.path.join(
2053- os.path.dirname(__file__),
2054- '../../cli/tests/data/foobar_0.1-1_source.changes'))
2055+ cwd = os.path.join(HERE, '../cli')
2056+ changes = os.path.abspath(os.path.join(HERE,
2057+ '../cli/tests/data/foobar_0.1-1_source.changes'))
2058 args = ('python ubuntu-ci create_ticket -t "New feature" -b 1234 -o '
2059 'someone@example.com -d "This is a cool new feature" -a foo '
2060 '-s %s' % changes)
2061@@ -39,18 +55,18 @@
2062 subprocess.check_call(args, shell=True)
2063
2064
2065-class IntegrationTest(DeployerTest):
2066+class IntegrationTest(deployers.DeployerTest):
2067 """Integration tests for ticket-system service run on a juju deployment"""
2068
2069- deployer_cfg = os.path.join(DEPLOYER_DIR, 'ticket-system.yaml')
2070+ deployer_cfg = 'ticket-system.yaml'
2071
2072 def setUp(self):
2073 super(IntegrationTest, self).setUp()
2074 self.temp_dir = create_virtual_env()
2075- p = os.path.join(os.path.dirname(__file__), '../../ci-utils/setup.py')
2076+ p = os.path.join(HERE, '../ci-utils/setup.py')
2077 args = '%s develop' % p
2078 run_virtual_env(self.temp_dir, args)
2079- p = os.path.join(os.path.dirname(__file__), '../../cli/setup.py')
2080+ p = os.path.join(HERE, '../cli/setup.py')
2081 args = '%s develop' % p
2082 run_virtual_env(self.temp_dir, args)
2083 config_url = 'http://{}:{}'
2084@@ -94,5 +110,6 @@
2085 self.assertEqual(resp['status'], '200')
2086 self.assertIn('<a href="/" class="first ">Ticket List</a>', content)
2087
2088+
2089 if __name__ == "__main__":
2090 unittest.main()
2091
2092=== renamed file 'tests/juju_gui/test.py' => 'tests/test_juju_gui.py'
2093--- tests/juju_gui/test.py 2014-01-29 17:27:07 +0000
2094+++ tests/test_juju_gui.py 2014-04-15 12:18:41 +0000
2095@@ -1,17 +1,30 @@
2096 #!/usr/bin/env python
2097-
2098-import os
2099+# Ubuntu Continuous Integration Engine
2100+# Copyright 2014 Canonical Ltd.
2101+
2102+# This program is free software: you can redistribute it and/or modify it
2103+# under the terms of the GNU Affero General Public License version 3, as
2104+# published by the Free Software Foundation.
2105+
2106+# This program is distributed in the hope that it will be useful, but
2107+# WITHOUT ANY WARRANTY; without even the implied warranties of
2108+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2109+# PURPOSE. See the GNU Affero General Public License for more details.
2110+
2111+# You should have received a copy of the GNU Affero General Public License
2112+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2113+
2114 import unittest
2115 import urllib2
2116
2117
2118-from tests.deployers import DEPLOYER_DIR, DeployerTest
2119-
2120-
2121-class JujuGuiTest(DeployerTest):
2122+import deployers
2123+
2124+
2125+class JujuGuiTest(deployers.DeployerTest):
2126 """Integration tests for juju-gui service run on a juju deployment"""
2127
2128- deployer_cfg = os.path.join(DEPLOYER_DIR, 'juju-gui.yaml')
2129+ deployer_cfg = 'juju-gui.yaml'
2130
2131 def get_unit(self):
2132 unit = self.status['services']['ci-juju-gui']['units'].values()[0]
2133
2134=== renamed file 'tests/ppa_assigner/test.py' => 'tests/test_ppa_assigner.py'
2135--- tests/ppa_assigner/test.py 2014-03-14 10:37:28 +0000
2136+++ tests/test_ppa_assigner.py 2014-04-15 12:18:41 +0000
2137@@ -1,18 +1,32 @@
2138 #!/usr/bin/env python
2139-
2140+# Ubuntu Continuous Integration Engine
2141+# Copyright 2013, 2014 Canonical Ltd.
2142+
2143+# This program is free software: you can redistribute it and/or modify it
2144+# under the terms of the GNU Affero General Public License version 3, as
2145+# published by the Free Software Foundation.
2146+
2147+# This program is distributed in the hope that it will be useful, but
2148+# WITHOUT ANY WARRANTY; without even the implied warranties of
2149+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2150+# PURPOSE. See the GNU Affero General Public License for more details.
2151+
2152+# You should have received a copy of the GNU Affero General Public License
2153+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2154+
2155+import httplib2
2156 import json
2157 import os
2158 import unittest
2159
2160-import httplib2
2161-
2162-from tests.deployers import DEPLOYER_DIR, DeployerTest
2163-
2164-
2165-class PPAAssignerTest(DeployerTest):
2166+
2167+import deployers
2168+
2169+
2170+class PPAAssignerTest(deployers.DeployerTest):
2171 """Integration tests for ppa-assigner service run on a juju deployment"""
2172
2173- deployer_cfg = os.path.join(DEPLOYER_DIR, 'ppa-assigner.yaml')
2174+ deployer_cfg = 'ppa-assigner.yaml'
2175
2176 def get_server_status_and_content(self, url):
2177 final_url = url.format(*self.get_ip_and_port('ppa-django'))
2178@@ -134,5 +148,6 @@
2179 self.assertEqual('/api/v1/ppa/2/',
2180 content['objects'][0]['resource_uri'])
2181
2182+
2183 if __name__ == "__main__":
2184 unittest.main()
2185
2186=== removed directory 'tests/test_runner'
2187=== renamed file 'tests/test_runner/test.py' => 'tests/test_test_runner.py'
2188--- tests/test_runner/test.py 2014-04-02 11:56:54 +0000
2189+++ tests/test_test_runner.py 2014-04-15 12:18:41 +0000
2190@@ -1,18 +1,31 @@
2191 #!/usr/bin/env python
2192+# Ubuntu Continuous Integration Engine
2193+# Copyright 2014 Canonical Ltd.
2194+
2195+# This program is free software: you can redistribute it and/or modify it
2196+# under the terms of the GNU Affero General Public License version 3, as
2197+# published by the Free Software Foundation.
2198+
2199+# This program is distributed in the hope that it will be useful, but
2200+# WITHOUT ANY WARRANTY; without even the implied warranties of
2201+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2202+# PURPOSE. See the GNU Affero General Public License for more details.
2203+
2204+# You should have received a copy of the GNU Affero General Public License
2205+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2206
2207 import json
2208-import os
2209 import unittest
2210
2211 import httplib2
2212
2213-from tests import deployers
2214+import deployers
2215
2216
2217 class TestTestRunner(deployers.DeployerTest):
2218 """Integration tests for test runner service run on a juju deployment"""
2219
2220- deployer_cfg = os.path.join(deployers.DEPLOYER_DIR, 'test-runner.yaml')
2221+ deployer_cfg = 'test-runner.yaml'
2222
2223 def post_test_image(self, ticket_id, image_id, package_list,
2224 progress_trigger):
2225
2226=== renamed file 'tests/ticket_system/test.py' => 'tests/test_ticket_system.py'
2227--- tests/ticket_system/test.py 2014-04-01 15:02:54 +0000
2228+++ tests/test_ticket_system.py 2014-04-15 12:18:41 +0000
2229@@ -1,18 +1,31 @@
2230 #!/usr/bin/env python
2231+# Ubuntu Continuous Integration Engine
2232+# Copyright 2014 Canonical Ltd.
2233+
2234+# This program is free software: you can redistribute it and/or modify it
2235+# under the terms of the GNU Affero General Public License version 3, as
2236+# published by the Free Software Foundation.
2237+
2238+# This program is distributed in the hope that it will be useful, but
2239+# WITHOUT ANY WARRANTY; without even the implied warranties of
2240+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2241+# PURPOSE. See the GNU Affero General Public License for more details.
2242+
2243+# You should have received a copy of the GNU Affero General Public License
2244+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2245
2246 import json
2247-import os
2248 import unittest
2249
2250 import httplib2
2251
2252-from tests.deployers import DEPLOYER_DIR, DeployerTest
2253-
2254-
2255-class TicketSystemTest(DeployerTest):
2256+import deployers
2257+
2258+
2259+class TicketSystemTest(deployers.DeployerTest):
2260 """Integration tests for ticket-system service run on a juju deployment"""
2261
2262- deployer_cfg = os.path.join(DEPLOYER_DIR, 'ticket-system.yaml')
2263+ deployer_cfg = 'ticket-system.yaml'
2264
2265 def get_server_status_and_content(self, url):
2266 final_url = url.format(*self.get_ip_and_port('ts-django'))
2267
2268=== removed directory 'tests/ticket_system'
2269=== modified file 'ticket_system/people/tests.py'
2270--- ticket_system/people/tests.py 2014-02-08 10:43:30 +0000
2271+++ ticket_system/people/tests.py 2014-04-15 12:18:41 +0000
2272@@ -23,7 +23,7 @@
2273 if 'people' in settings.LOCAL_APPS:
2274 return std_tests
2275 else:
2276- # if people is not in LOCAL_APPS the tests cannot pass so get rid of
2277+ # If 'people' is not in LOCAL_APPS the tests cannot pass so get rid of
2278 # them by returning an empty test suite.
2279 return loader.suiteClass()
2280

Subscribers

People subscribed via source and target branches