Merge lp:~benji/charms/oneiric/buildbot-master/more-tests into lp:~yellow/charms/oneiric/buildbot-master/trunk

Proposed by Benji York
Status: Merged
Approved by: Benji York
Approved revision: 19
Merge reported by: Benji York
Merged at revision: not available
Proposed branch: lp:~benji/charms/oneiric/buildbot-master/more-tests
Merge into: lp:~yellow/charms/oneiric/buildbot-master/trunk
Diff against target: 375 lines (+229/-28)
5 files modified
HACKING.txt (+5/-1)
hooks/helpers.py (+45/-11)
hooks/tests.py (+26/-1)
tests/buildbot-master.test (+35/-15)
tests/test.cfg (+118/-0)
To merge this branch: bzr merge lp:~benji/charms/oneiric/buildbot-master/more-tests
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+92123@code.launchpad.net

Description of the change

This branch primarily adds a basic smoke test and fixes some bugs that
prevented the smoke test from passing.

- add a boostrap step to HACKING.txt
- change a (short circuiting) logical or into a bitwise
  (non-short-circuiting) or to fix a startup race condition
- add some log helpers for entering/exiting scripts
- spiff up the run() function so stderr doesn't leak to the console when
  running tests
- use a try/finally to be sure script exits are logged
- add a testing buildbot config

Tests:

Both test suites pass:

    python hooks/tests.py

    RESOLVE_TEST_CHARMS=1 tests/buildbot-master.test

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Hi Benji,

Some of this code already landed in my branch. Hope that doesn't complicate things.

Also, the branch I landed for gmb and frankban made changes to unit_info. You may want to look at what is in trunk before attempting the merge.

As we discussed this morning there is a move afoot to follow more of PEP-8 than LP coding style so perhaps you could update the tests to getRidOfCamelCase in_favor_of_underscores?

typo: occurs not ocurrs

redundant not redunant

There is now an 'encode_file' helper if you want to use it.

review: Approve (code)
20. By Benji York

fix spelling errors

21. By Benji York

merge from trunk and incorporate some review suggestions

22. By Benji York

simplify

23. By Benji York

use the encode_file helper

Revision history for this message
Benji York (benji) wrote :

> Also, the branch I landed for gmb and frankban made changes to unit_info. You
> may want to look at what is in trunk before attempting the merge.

Thanks for the heads-up. I was able to merge without too much trouble.

> As we discussed this morning there is a move afoot to follow more of PEP-8
> than LP coding style so perhaps you could update the tests to
> getRidOfCamelCase in_favor_of_underscores?

Good, I like underscores better. Done.

> typo: occurs not ocurrs
>
> redundant not redunant

Thanks. Fixed.

> There is now an 'encode_file' helper if you want to use it.

That feels like taking the helpers a bit far, but if we already have it,
I guess I'll follow along.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'HACKING.txt'
2--- HACKING.txt 2012-02-02 18:35:04 +0000
3+++ HACKING.txt 2012-02-08 22:09:17 +0000
4@@ -14,7 +14,11 @@
5 real juju executable, naming it "juju". Edit the CHARM_TEST_REPO variable
6 therein to reflect the location of the charm repo from step 1.
7
8-3) Run the tests: RESOLVE_TEST_CHARMS=1 tests/buildbot-master.test
9+3) Bootstrap the juju environment if not already done:
10+
11+ juju bootstrap
12+
13+4) Run the tests: RESOLVE_TEST_CHARMS=1 tests/buildbot-master.test
14
15
16 Running the charm helper tests
17
18=== modified file 'hooks/helpers.py'
19--- hooks/helpers.py 2012-02-08 20:56:21 +0000
20+++ hooks/helpers.py 2012-02-08 22:09:17 +0000
21@@ -21,6 +21,7 @@
22 'unit_info',
23 ]
24
25+from textwrap import dedent
26 import base64
27 import json
28 import os
29@@ -28,10 +29,25 @@
30 import StringIO
31 import subprocess
32 import sys
33-from textwrap import dedent
34+import time
35 import yaml
36
37
38+def run(*args):
39+ """Run the command with the given arguments.
40+
41+ The first argument is the path to the command to run, subsequent arguments
42+ are command-line arguments to be passed.
43+ """
44+ process = subprocess.Popen(args, stdout=subprocess.PIPE,
45+ stderr=subprocess.PIPE, close_fds=True)
46+ stdout, stderr = process.communicate()
47+ if process.returncode:
48+ raise subprocess.CalledProcessError(
49+ process.returncode, repr(args), output=stdout+stderr)
50+ return stdout
51+
52+
53 def command(*base_args):
54 """Return a callable that will run the given command with any arguments.
55
56@@ -52,7 +68,7 @@
57 """
58 def callable_command(*args):
59 all_args = base_args + args
60- return subprocess.check_output(all_args, shell=False)
61+ return run(*all_args)
62
63 return callable_command
64
65@@ -61,10 +77,6 @@
66 apt_get_install = command('apt-get', 'install', '-y', '--force-yes')
67
68
69-def run(*args):
70- return subprocess.check_output(args, shell=False)
71-
72-
73 def get_config():
74 config_get = command('config-get', '--format=json')
75 return json.loads(config_get())
76@@ -75,9 +87,9 @@
77 run('apt-add-repository', extra_repository)
78 run('apt-get', 'update')
79 except subprocess.CalledProcessError as e:
80- log("Error adding repository %s" % extra_repository)
81+ log('Error adding repository: ' + extra_repository)
82 log(e)
83- sys.exit(-1)
84+ raise
85
86
87 def relation_get(*args):
88@@ -126,16 +138,16 @@
89 return line.split('=')[1].strip('"\' ')
90
91
92-def _script_name():
93+def script_name():
94 return os.path.basename(sys.argv[0])
95
96
97 def log_entry():
98- log("<-- Entering {}".format(_script_name()))
99+ log("<-- Entering {}".format(script_name()))
100
101
102 def log_exit():
103- log("--> Exiting {}".format(_script_name()))
104+ log("--> Exiting {}".format(script_name()))
105
106
107 class DictDiffer:
108@@ -188,6 +200,7 @@
109 new[k] = self.current_dict.get(k)
110 old[k] = self.past_dict.get(k)
111 return "%s -> %s" % (old, new)
112+
113 def __str__(self):
114 if self.modified:
115 s = dedent("""\
116@@ -204,6 +217,27 @@
117 return s
118
119
120+def unit_info(service_name, item_name, data=None):
121+ if data is None:
122+ data = yaml.safe_load(run('juju', 'status'))
123+ services = data['services'][service_name]
124+ units = services['units']
125+ item = units.items()[0][1][item_name]
126+ return item
127+
128+
129+def wait_for_unit(service_name, timeout=120):
130+ start_time = time.time()
131+ while True:
132+ state = unit_info(service_name, 'state')
133+ if 'error' in state or state == 'started':
134+ break
135+ if time.time() - start_time >= timeout:
136+ raise RuntimeError('timeout waiting for service to start')
137+ time.sleep(0.1)
138+ if state != 'started':
139+ raise RuntimeError('unit did not start, state: ' + state)
140+
141 class Serializer:
142 """Handle JSON (de)serialization."""
143
144
145=== modified file 'hooks/tests.py'
146--- hooks/tests.py 2012-02-07 14:37:13 +0000
147+++ hooks/tests.py 2012-02-08 22:09:17 +0000
148@@ -3,11 +3,36 @@
149 from subprocess import CalledProcessError
150
151 from helpers import (
152+ DictDiffer,
153 command,
154- DictDiffer,
155+ run,
156 unit_info,
157 )
158
159+from subprocess import CalledProcessError
160+
161+class TestRun(unittest.TestCase):
162+
163+ def testSimpleCommand(self):
164+ # Running a simple command (ls) works and running the command
165+ # produces a string.
166+ self.assertIsInstance(run('/bin/ls'), str)
167+
168+ def testStdoutReturned(self):
169+ # Running a simple command (ls) works and running the command
170+ # produces a string.
171+ self.assertIn('Usage:', run('/bin/ls', '--help'))
172+
173+ def testSimpleCommand(self):
174+ # If an error occurs a CalledProcessError is raised with the return
175+ # code, command executed, and the output of the command.
176+ with self.assertRaises(CalledProcessError) as info:
177+ run('ls', '--not a valid switch')
178+ exception = info.exception
179+ self.assertEqual(2, exception.returncode)
180+ self.assertEqual("('ls', '--not a valid switch')", exception.cmd)
181+ self.assertIn('unrecognized option', exception.output)
182+
183
184 class TestCommand(unittest.TestCase):
185
186
187=== modified file 'tests/buildbot-master.test'
188--- tests/buildbot-master.test 2012-02-07 12:35:06 +0000
189+++ tests/buildbot-master.test 2012-02-08 22:09:17 +0000
190@@ -1,27 +1,47 @@
191 #!/usr/bin/python
192-
193 # Copyright 2012 Canonical Ltd. This software is licensed under the
194 # GNU Affero General Public License version 3 (see the file LICENSE).
195
196-from helpers import command, run, unit_info
197-import time
198+from helpers import (
199+ command,
200+ encode_file,
201+ run,
202+ unit_info,
203+ wait_for_unit,
204+ )
205+import base64
206+import os.path
207 import unittest
208+import urllib2
209
210 juju = command('juju')
211
212-class testCharm(unittest.TestCase):
213-
214- def testDeploy(self):
215+class TestCharm(unittest.TestCase):
216+
217+ def tearDown(self):
218+ juju('destroy-service', 'buildbot-master')
219+
220+ def test_deploy(self):
221+ # See if the charm will actual deploy correctly. This test may be
222+ # made redundant by standard charm tests run by some future centralized
223+ # charm repository.
224+ juju('deploy', 'buildbot-master')
225+ wait_for_unit('buildbot-master')
226+ self.assertEqual(unit_info('buildbot-master', 'state'), 'started')
227+
228+ def test_port_opened(self):
229+ juju('deploy', 'buildbot-master')
230+ juju('set', 'buildbot-master', 'extra-packages=git')
231+ config_path = os.path.join(os.path.dirname(__file__), 'test.cfg')
232+ config = encode_file(config_path)
233+ juju('set', 'buildbot-master', 'config-file='+config)
234+ wait_for_unit('buildbot-master')
235+ addr = unit_info('buildbot-master', 'public-address')
236 try:
237- juju('deploy', 'buildbot-master')
238- while True:
239- status = unit_info('buildbot-master', 'state')
240- if 'error' in status or status == 'started':
241- break
242- time.sleep(0.1)
243- self.assertEqual(unit_info('buildbot-master', 'state'), 'started')
244- finally:
245- juju('destroy-service', 'buildbot-master')
246+ page = urllib2.urlopen('http://{}:8010'.format(addr)).read()
247+ except urllib2.URLError:
248+ self.fail('could not fetch buildbot master status page')
249+ self.assertIn('Welcome to the Buildbot', page)
250
251
252 if __name__ == '__main__':
253
254=== added file 'tests/test.cfg'
255--- tests/test.cfg 1970-01-01 00:00:00 +0000
256+++ tests/test.cfg 2012-02-08 22:09:17 +0000
257@@ -0,0 +1,118 @@
258+# -*- python -*-
259+# ex: set syntax=python:
260+
261+# This is a sample buildmaster config file. It must be installed as
262+# 'master.cfg' in your buildmaster's base directory.
263+
264+# This is the dictionary that the buildmaster pays attention to. We also use
265+# a shorter alias to save typing.
266+c = BuildmasterConfig = {}
267+
268+####### BUILDSLAVES
269+
270+# The 'slaves' list defines the set of recognized buildslaves. Each element is
271+# a BuildSlave object, specifying a username and password. The same username and
272+# password must be configured on the slave.
273+from buildbot.buildslave import BuildSlave
274+c['slaves'] = []
275+
276+# 'slavePortnum' defines the TCP port to listen on for connections from slaves.
277+# This must match the value configured into the buildslaves (with their
278+# --master option)
279+c['slavePortnum'] = 9989
280+
281+####### CHANGESOURCES
282+
283+# the 'change_source' setting tells the buildmaster how it should find out
284+# about source code changes. Here we point to the buildbot clone of pyflakes.
285+
286+from buildbot.changes.gitpoller import GitPoller
287+c['change_source'] = GitPoller(
288+ 'git://github.com/buildbot/pyflakes.git',
289+ branch='master', pollinterval=1200)
290+
291+####### SCHEDULERS
292+
293+# Configure the Schedulers, which decide how to react to incoming changes. In this
294+# case, just kick off a 'runtests' build
295+
296+from buildbot.scheduler import Scheduler
297+c['schedulers'] = []
298+c['schedulers'].append(Scheduler(name="all", branch=None,
299+ treeStableTimer=None,
300+ builderNames=["runtests"]))
301+
302+####### BUILDERS
303+
304+# The 'builders' list defines the Builders, which tell Buildbot how to perform a build:
305+# what steps, and which slaves can execute them. Note that any particular build will
306+# only take place on one slave.
307+
308+from buildbot.process.factory import BuildFactory
309+from buildbot.steps.source import Git
310+from buildbot.steps.shell import ShellCommand
311+
312+factory = BuildFactory()
313+# check out the source
314+factory.addStep(Git(repourl='git://github.com/buildbot/pyflakes.git', mode='copy'))
315+# run the tests (note that this will require that 'trial' is installed)
316+factory.addStep(ShellCommand(command=["trial", "pyflakes"]))
317+
318+from buildbot.config import BuilderConfig
319+
320+c['builders'] = [
321+ BuilderConfig(name="runtests",
322+ # Buildbot enforces that the slavenames list must not be empty. Our
323+ # wrapper will remove the empty string and replace it with proper values.
324+ slavenames=[''],
325+ factory=factory),
326+ ]
327+
328+####### STATUS TARGETS
329+
330+# 'status' is a list of Status Targets. The results of each build will be
331+# pushed to these targets. buildbot/status/*.py has a variety to choose from,
332+# including web pages, email senders, and IRC bots.
333+
334+c['status'] = []
335+
336+from buildbot.status import html
337+from buildbot.status.web import auth, authz
338+authz_cfg=authz.Authz(
339+ # change any of these to True to enable; see the manual for more
340+ # options
341+ gracefulShutdown = False,
342+ forceBuild = True, # use this to test your slave once it is set up
343+ forceAllBuilds = False,
344+ pingBuilder = False,
345+ stopBuild = False,
346+ stopAllBuilds = False,
347+ cancelPendingBuild = False,
348+)
349+c['status'].append(html.WebStatus(http_port=8010, authz=authz_cfg))
350+
351+####### PROJECT IDENTITY
352+
353+# the 'projectName' string will be used to describe the project that this
354+# buildbot is working on. For example, it is used as the title of the
355+# waterfall HTML page. The 'projectURL' string will be used to provide a link
356+# from buildbot HTML pages to your project's home page.
357+
358+c['projectName'] = "Pyflakes"
359+c['projectURL'] = "http://divmod.org/trac/wiki/DivmodPyflakes"
360+
361+# the 'buildbotURL' string should point to the location where the buildbot's
362+# internal web server (usually the html.WebStatus page) is visible. This
363+# typically uses the port number set in the Waterfall 'status' entry, but
364+# with an externally-visible host name which the buildbot cannot figure out
365+# without some help.
366+
367+c['buildbotURL'] = "http://localhost:8010/"
368+
369+####### DB URL
370+
371+# This specifies what database buildbot uses to store change and scheduler
372+# state. You can leave this at its default for all but the largest
373+# installations.
374+c['db_url'] = "sqlite:///state.sqlite"
375+

Subscribers

People subscribed via source and target branches

to all changes: