Merge lp:~stub/charms/precise/postgresql/use-amulet into lp:charms/postgresql

Proposed by Stuart Bishop
Status: Rejected
Rejected by: Stuart Bishop
Proposed branch: lp:~stub/charms/precise/postgresql/use-amulet
Merge into: lp:charms/postgresql
Prerequisite: lp:~stub/charms/precise/postgresql/syslog
Diff against target: 240 lines (+166/-8)
4 files modified
test.py (+17/-7)
testing/amuletfixture.py (+133/-0)
testing/jujufixture.py (+1/-1)
tests/00_setup.test (+15/-0)
To merge this branch: bzr merge lp:~stub/charms/precise/postgresql/use-amulet
Reviewer Review Type Date Requested Status
Stuart Bishop (community) Needs Fixing
Review via email: mp+206181@code.launchpad.net

This proposal supersedes a proposal from 2014-02-11.

Description of the change

Change to using Amulet instead of our own bespoke test harness where possible.

To post a comment you must log in.
Revision history for this message
Stuart Bishop (stub) wrote : Posted in a previous version of this proposal

This spike is looking promising.

Issues to address:
  - Reuse of machines.
    - We can detect 'free' machines, and inject the machine allocations into the juju-deployer structure.
    - add_unit will need to use a 'free' machine if there is one.
  - sentry.wait() needs to detect failed hooks and abort.
  - Should amulet actually be using juju-deployer? There is stuff in there we don't want, such as building a bzr branch of the charm.

review: Needs Fixing
Revision history for this message
Stuart Bishop (stub) :
review: Needs Fixing
170. By Stuart Bishop

Merged syslog into use-amulet.

171. By Stuart Bishop

Merged syslog into use-amulet.

172. By Stuart Bishop

Merged syslog into use-amulet.

173. By Stuart Bishop

resolve conflicts

174. By Stuart Bishop

Merged syslog into use-amulet.

175. By Stuart Bishop

Merged syslog into use-amulet.

176. By Stuart Bishop

Merged syslog into use-amulet.

177. By Stuart Bishop

resolve conflicts

178. By Stuart Bishop

Merged syslog into use-amulet.

179. By Stuart Bishop

Merged syslog into use-amulet.

180. By Stuart Bishop

Merged syslog into use-amulet.

181. By Stuart Bishop

Merged syslog into use-amulet.

182. By Stuart Bishop

Merged syslog into use-amulet.

183. By Stuart Bishop

Merged syslog into use-amulet.

184. By Stuart Bishop

Merged syslog into use-amulet.

185. By Stuart Bishop

Merged syslog into use-amulet.

186. By Stuart Bishop

Merged syslog into use-amulet.

187. By Stuart Bishop

Merged syslog into use-amulet.

188. By Stuart Bishop

Merged syslog into use-amulet.

189. By Stuart Bishop

Merged syslog into use-amulet.

190. By Stuart Bishop

Merged syslog into use-amulet.

191. By Stuart Bishop

Merged syslog into use-amulet.

192. By Stuart Bishop

Merged syslog into use-amulet.

193. By Stuart Bishop

Merged syslog into use-amulet.

194. By Stuart Bishop

Merged syslog into use-amulet.

195. By Stuart Bishop

Merged syslog into use-amulet.

196. By Stuart Bishop

Merged syslog into use-amulet.

197. By Stuart Bishop

Merged syslog into use-amulet.

198. By Stuart Bishop

Merged syslog into use-amulet.

199. By Stuart Bishop

Merged syslog into use-amulet.

200. By Stuart Bishop

Merged syslog into use-amulet.

201. By Stuart Bishop

Merged syslog into use-amulet.

202. By Stuart Bishop

mergr trunk

Unmerged revisions

202. By Stuart Bishop

mergr trunk

201. By Stuart Bishop

Merged syslog into use-amulet.

200. By Stuart Bishop

Merged syslog into use-amulet.

199. By Stuart Bishop

Merged syslog into use-amulet.

198. By Stuart Bishop

Merged syslog into use-amulet.

197. By Stuart Bishop

Merged syslog into use-amulet.

196. By Stuart Bishop

Merged syslog into use-amulet.

195. By Stuart Bishop

Merged syslog into use-amulet.

194. By Stuart Bishop

Merged syslog into use-amulet.

193. By Stuart Bishop

Merged syslog into use-amulet.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'test.py'
2--- test.py 2014-10-24 05:42:49 +0000
3+++ test.py 2014-10-24 05:42:49 +0000
4@@ -1,4 +1,4 @@
5-#!/usr/bin/python
6+#!/usr/bin/python3
7
8 """
9 Test the PostgreSQL charm.
10@@ -23,6 +23,7 @@
11 import psycopg2
12 import testtools
13
14+from testing.amuletfixture import AmuletFixture
15 from testing.jujufixture import JujuFixture, run
16
17
18@@ -92,6 +93,8 @@
19 # Tests may add or change options.
20 self.pg_config = dict(version=version, pgdg=use_pgdg)
21
22+ self.amulet = self.useFixture(AmuletFixture())
23+
24 # Mirror charmhelpers into our support charms, since charms
25 # can't symlink out of their subtree.
26 here = os.path.abspath(os.path.dirname(__file__))
27@@ -307,8 +310,8 @@
28 '''
29 # Which psql unit we are going to query from.
30 if psql_unit is None:
31- psql_unit = (
32- self.juju.status['services']['psql']['units'].keys()[0])
33+ psql_unit = list(
34+ self.juju.status['services']['psql']['units'].keys())[0]
35
36 full_rel_info = self.juju.relation_info(psql_unit)
37
38@@ -409,10 +412,17 @@
39
40 def test_basic(self):
41 '''Connect to a a single unit service via the db relationship.'''
42- self.juju.deploy(TEST_CHARM, 'postgresql', config=self.pg_config)
43- self.juju.deploy(PSQL_CHARM, 'psql')
44- self.juju.do(['add-relation', 'postgresql:db', 'psql:db'])
45- self.wait_until_ready(['postgresql/0'])
46+ ## self.juju.deploy(TEST_CHARM, 'postgresql', config=self.pg_config)
47+ ## self.juju.deploy(PSQL_CHARM, 'psql')
48+ ## self.juju.do(['add-relation', 'postgresql:db', 'psql:db'])
49+ ## self.wait_until_ready()
50+
51+ self.amulet.add('postgresql')
52+ self.amulet.configure('postgresql', self.pg_config)
53+ self.amulet.add('psql', PSQL_CHARM)
54+ self.amulet.relate('postgresql:db', 'psql:db')
55+ self.amulet.setup()
56+ self.amulet.sentry.wait()
57
58 result = self.sql('SELECT TRUE')
59 self.assertEqual(result, [(True,)])
60
61=== added file 'testing/amuletfixture.py'
62--- testing/amuletfixture.py 1970-01-01 00:00:00 +0000
63+++ testing/amuletfixture.py 2014-10-24 05:42:49 +0000
64@@ -0,0 +1,133 @@
65+import json
66+import subprocess
67+import time
68+
69+import amulet.helpers
70+import fixtures
71+from testtools.content import text_content
72+
73+
74+class EnvironmentResetError(Exception):
75+ pass
76+
77+
78+class MyDeployment(amulet.Deployment):
79+
80+ def setup(self):
81+ # Specify an empty BZR_HOME. juju-deployer wants to run bzr
82+ # commands, and we don't want to use the user's normal bzr
83+ # configuration or silly things happen like GPG signing popups.
84+ with fixtures.TempDir() as bzr_home:
85+ with fixtures.EnvironmentVariable('BZR_HOME', bzr_home.path):
86+ super(MyDeployment, self).setup()
87+
88+ def do(self, cmd, timeout=600):
89+ """Run an arbitrary juju command.
90+
91+ cmd is the juju command line as a list of arguments. It is
92+ automatically prefixed with 'juju' and suffixed with the environment
93+ to operate in.
94+
95+ eg. amulet.do(['destroy-unit', 'foo/2'])
96+ """
97+ cmd = ['juju'] + cmd + ['--environment', self.juju_env]
98+ self._run(cmd, timeout)
99+
100+ def query(self, cmd, timeout=600):
101+ """Run a juju query, returning the decoded result as structured data.
102+
103+ cmd is the juju command line as a list of arguments. It is
104+ automatically prefixed with 'juju', the environment details,
105+ and '--format=json'.
106+
107+ eg. status = amulet.query(['status'])
108+ """
109+ cmd = ['juju'] + cmd + [
110+ '--environment', self.juju_env, '--format', 'json']
111+ return json.loads(self._run(cmd, timeout).decode('UTF-8'))
112+
113+ def add_unit(self, service, units=1):
114+ if service not in self.services:
115+ raise ValueError('Service {0} is not deployed'.format(service))
116+ self.services[service]['num_units'] += units
117+ self.do(['add-unit', '-n', str(int(units)), service])
118+
119+ def destroy_unit(self, units):
120+ if isinstance(units, basestring):
121+ units = [units]
122+ for unit in units:
123+ service = unit.split('/')[0]
124+ if service not in self.services:
125+ raise ValueError('Service {0} is not deployed'.format(service))
126+ self.service[service]['num_units'] -= 1
127+ self.do(['destroy-unit'] + units)
128+
129+ def cleanup(self, force=False):
130+ # Temporarily work around a bug in Deployer.cleanup()
131+ self._sentry = self._sentries
132+ super(MyDeployment, self).cleanup()
133+
134+ # Tear down any services left running that we know we spawned.
135+ while True:
136+ found_services = False
137+
138+ status = self.query(['status'])
139+
140+ # Kill any services this fixture knows it started.
141+ for service_name, service in status.get('services', {}).items():
142+ if service_name.replace('-sentry', '') in self.services:
143+ found_services = True
144+ if service.get('life', '') not in ('dying', 'dead'):
145+ self.do(['destroy-service', service_name])
146+
147+ # Handle failed units.
148+ for unit_name, unit in service.get('units', {}).items():
149+ if unit.get('agent-state', None) == 'error':
150+ if force:
151+ try:
152+ self.do(['resolved', unit_name])
153+ except subprocess.CalledProcessError:
154+ # More race conditions in juju. A
155+ # previous 'resolved' call may cause a
156+ # subsequent one to fail if it is still
157+ # being processed. However, we need to
158+ # keep retrying because after a
159+ # 'resolved' has been processed,
160+ # other hooks may run and fail.
161+ pass
162+ else:
163+ raise EnvironmentResetError(
164+ 'Failed unit {}'.format(unit_name))
165+ if not found_services:
166+ break
167+ time.sleep(1)
168+
169+ def _run(self, cmd, timeout):
170+ # We want to keep test runners happy, so ensure we don't spam
171+ # stdout and stderr.
172+ with amulet.helpers.timeout(timeout):
173+ p = subprocess.Popen(
174+ cmd, stdin=subprocess.PIPE,
175+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
176+ (out, err) = p.communicate()
177+ if p.returncode != 0:
178+ self.addDetail('stdout', text_content(out.decode('UTF-8')))
179+ self.addDetail('stderr', text_content(err.decode('UTF-8')))
180+ raise subprocess.CalledProcessError(p.returncode, cmd, err)
181+ return out
182+
183+
184+class AmuletFixture(fixtures.Fixture, MyDeployment):
185+ """An Amulet Deployment as a Fixture."""
186+ def setUp(self):
187+ # Does nothing, as initialized in constructor.
188+ super(AmuletFixture, self).setUp()
189+
190+ def cleanUp(self):
191+ try:
192+ super(AmuletFixture, self).cleanup()
193+ except EnvironmentResetError:
194+ super(AmuletFixture, self).cleanup(force=True)
195+ raise
196+
197+
198
199=== modified file 'testing/jujufixture.py'
200--- testing/jujufixture.py 2014-10-14 10:12:02 +0000
201+++ testing/jujufixture.py 2014-10-24 05:42:49 +0000
202@@ -42,7 +42,7 @@
203 cmd = ['juju'] + cmd + ['--format=json']
204 out = run(self, cmd)
205 if out:
206- return json.loads(out)
207+ return json.loads(out.decode('UTF-8'))
208 return None
209
210 def deploy(self, charm, name=None, num_units=1, config=None):
211
212=== modified file 'tests/00_setup.test'
213--- tests/00_setup.test 2014-04-01 12:15:48 +0000
214+++ tests/00_setup.test 2014-10-24 05:42:49 +0000
215@@ -1,6 +1,14 @@
216 #!/bin/sh
217
218+dpkg -s amulet
219+if [ $? -ne 0 ]; then
220+ # Install the Amulet testing harness.
221+ sudo add-apt-repository -y ppa:juju/stable
222+ sudo apt-get update
223+fi
224+
225 sudo apt-get install -y \
226+ amulet
227 python-flake8 \
228 python-fixtures \
229 python-jinja2 \
230@@ -11,3 +19,10 @@
231 python-yaml \
232 pgtune
233
234+sudo apt-get install -y \
235+ python3-flake8 \
236+ python3-fixtures \
237+ python3-jinja2 \
238+ python3-psycopg2 \
239+ python3-testtools \
240+ python3-yaml

Subscribers

People subscribed via source and target branches