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
=== modified file 'HACKING.txt'
--- HACKING.txt 2012-02-02 18:35:04 +0000
+++ HACKING.txt 2012-02-08 22:09:17 +0000
@@ -14,7 +14,11 @@
14 real juju executable, naming it "juju". Edit the CHARM_TEST_REPO variable14 real juju executable, naming it "juju". Edit the CHARM_TEST_REPO variable
15 therein to reflect the location of the charm repo from step 1.15 therein to reflect the location of the charm repo from step 1.
1616
173) Run the tests: RESOLVE_TEST_CHARMS=1 tests/buildbot-master.test173) Bootstrap the juju environment if not already done:
18
19 juju bootstrap
20
214) Run the tests: RESOLVE_TEST_CHARMS=1 tests/buildbot-master.test
1822
1923
20Running the charm helper tests24Running the charm helper tests
2125
=== modified file 'hooks/helpers.py'
--- hooks/helpers.py 2012-02-08 20:56:21 +0000
+++ hooks/helpers.py 2012-02-08 22:09:17 +0000
@@ -21,6 +21,7 @@
21 'unit_info',21 'unit_info',
22 ]22 ]
2323
24from textwrap import dedent
24import base6425import base64
25import json26import json
26import os27import os
@@ -28,10 +29,25 @@
28import StringIO29import StringIO
29import subprocess30import subprocess
30import sys31import sys
31from textwrap import dedent32import time
32import yaml33import yaml
3334
3435
36def run(*args):
37 """Run the command with the given arguments.
38
39 The first argument is the path to the command to run, subsequent arguments
40 are command-line arguments to be passed.
41 """
42 process = subprocess.Popen(args, stdout=subprocess.PIPE,
43 stderr=subprocess.PIPE, close_fds=True)
44 stdout, stderr = process.communicate()
45 if process.returncode:
46 raise subprocess.CalledProcessError(
47 process.returncode, repr(args), output=stdout+stderr)
48 return stdout
49
50
35def command(*base_args):51def command(*base_args):
36 """Return a callable that will run the given command with any arguments.52 """Return a callable that will run the given command with any arguments.
3753
@@ -52,7 +68,7 @@
52 """68 """
53 def callable_command(*args):69 def callable_command(*args):
54 all_args = base_args + args70 all_args = base_args + args
55 return subprocess.check_output(all_args, shell=False)71 return run(*all_args)
5672
57 return callable_command73 return callable_command
5874
@@ -61,10 +77,6 @@
61apt_get_install = command('apt-get', 'install', '-y', '--force-yes')77apt_get_install = command('apt-get', 'install', '-y', '--force-yes')
6278
6379
64def run(*args):
65 return subprocess.check_output(args, shell=False)
66
67
68def get_config():80def get_config():
69 config_get = command('config-get', '--format=json')81 config_get = command('config-get', '--format=json')
70 return json.loads(config_get())82 return json.loads(config_get())
@@ -75,9 +87,9 @@
75 run('apt-add-repository', extra_repository)87 run('apt-add-repository', extra_repository)
76 run('apt-get', 'update')88 run('apt-get', 'update')
77 except subprocess.CalledProcessError as e:89 except subprocess.CalledProcessError as e:
78 log("Error adding repository %s" % extra_repository)90 log('Error adding repository: ' + extra_repository)
79 log(e)91 log(e)
80 sys.exit(-1)92 raise
8193
8294
83def relation_get(*args):95def relation_get(*args):
@@ -126,16 +138,16 @@
126 return line.split('=')[1].strip('"\' ')138 return line.split('=')[1].strip('"\' ')
127139
128140
129def _script_name():141def script_name():
130 return os.path.basename(sys.argv[0])142 return os.path.basename(sys.argv[0])
131143
132144
133def log_entry():145def log_entry():
134 log("<-- Entering {}".format(_script_name()))146 log("<-- Entering {}".format(script_name()))
135147
136148
137def log_exit():149def log_exit():
138 log("--> Exiting {}".format(_script_name()))150 log("--> Exiting {}".format(script_name()))
139151
140152
141class DictDiffer:153class DictDiffer:
@@ -188,6 +200,7 @@
188 new[k] = self.current_dict.get(k)200 new[k] = self.current_dict.get(k)
189 old[k] = self.past_dict.get(k)201 old[k] = self.past_dict.get(k)
190 return "%s -> %s" % (old, new)202 return "%s -> %s" % (old, new)
203
191 def __str__(self):204 def __str__(self):
192 if self.modified:205 if self.modified:
193 s = dedent("""\206 s = dedent("""\
@@ -204,6 +217,27 @@
204 return s217 return s
205218
206219
220def unit_info(service_name, item_name, data=None):
221 if data is None:
222 data = yaml.safe_load(run('juju', 'status'))
223 services = data['services'][service_name]
224 units = services['units']
225 item = units.items()[0][1][item_name]
226 return item
227
228
229def wait_for_unit(service_name, timeout=120):
230 start_time = time.time()
231 while True:
232 state = unit_info(service_name, 'state')
233 if 'error' in state or state == 'started':
234 break
235 if time.time() - start_time >= timeout:
236 raise RuntimeError('timeout waiting for service to start')
237 time.sleep(0.1)
238 if state != 'started':
239 raise RuntimeError('unit did not start, state: ' + state)
240
207class Serializer:241class Serializer:
208 """Handle JSON (de)serialization."""242 """Handle JSON (de)serialization."""
209243
210244
=== modified file 'hooks/tests.py'
--- hooks/tests.py 2012-02-07 14:37:13 +0000
+++ hooks/tests.py 2012-02-08 22:09:17 +0000
@@ -3,11 +3,36 @@
3from subprocess import CalledProcessError3from subprocess import CalledProcessError
44
5from helpers import (5from helpers import (
6 DictDiffer,
6 command,7 command,
7 DictDiffer,8 run,
8 unit_info,9 unit_info,
9 )10 )
1011
12from subprocess import CalledProcessError
13
14class TestRun(unittest.TestCase):
15
16 def testSimpleCommand(self):
17 # Running a simple command (ls) works and running the command
18 # produces a string.
19 self.assertIsInstance(run('/bin/ls'), str)
20
21 def testStdoutReturned(self):
22 # Running a simple command (ls) works and running the command
23 # produces a string.
24 self.assertIn('Usage:', run('/bin/ls', '--help'))
25
26 def testSimpleCommand(self):
27 # If an error occurs a CalledProcessError is raised with the return
28 # code, command executed, and the output of the command.
29 with self.assertRaises(CalledProcessError) as info:
30 run('ls', '--not a valid switch')
31 exception = info.exception
32 self.assertEqual(2, exception.returncode)
33 self.assertEqual("('ls', '--not a valid switch')", exception.cmd)
34 self.assertIn('unrecognized option', exception.output)
35
1136
12class TestCommand(unittest.TestCase):37class TestCommand(unittest.TestCase):
1338
1439
=== modified file 'tests/buildbot-master.test'
--- tests/buildbot-master.test 2012-02-07 12:35:06 +0000
+++ tests/buildbot-master.test 2012-02-08 22:09:17 +0000
@@ -1,27 +1,47 @@
1#!/usr/bin/python1#!/usr/bin/python
2
3# Copyright 2012 Canonical Ltd. This software is licensed under the2# Copyright 2012 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).3# GNU Affero General Public License version 3 (see the file LICENSE).
54
6from helpers import command, run, unit_info5from helpers import (
7import time6 command,
7 encode_file,
8 run,
9 unit_info,
10 wait_for_unit,
11 )
12import base64
13import os.path
8import unittest14import unittest
15import urllib2
916
10juju = command('juju')17juju = command('juju')
1118
12class testCharm(unittest.TestCase):19class TestCharm(unittest.TestCase):
1320
14 def testDeploy(self):21 def tearDown(self):
22 juju('destroy-service', 'buildbot-master')
23
24 def test_deploy(self):
25 # See if the charm will actual deploy correctly. This test may be
26 # made redundant by standard charm tests run by some future centralized
27 # charm repository.
28 juju('deploy', 'buildbot-master')
29 wait_for_unit('buildbot-master')
30 self.assertEqual(unit_info('buildbot-master', 'state'), 'started')
31
32 def test_port_opened(self):
33 juju('deploy', 'buildbot-master')
34 juju('set', 'buildbot-master', 'extra-packages=git')
35 config_path = os.path.join(os.path.dirname(__file__), 'test.cfg')
36 config = encode_file(config_path)
37 juju('set', 'buildbot-master', 'config-file='+config)
38 wait_for_unit('buildbot-master')
39 addr = unit_info('buildbot-master', 'public-address')
15 try:40 try:
16 juju('deploy', 'buildbot-master')41 page = urllib2.urlopen('http://{}:8010'.format(addr)).read()
17 while True:42 except urllib2.URLError:
18 status = unit_info('buildbot-master', 'state')43 self.fail('could not fetch buildbot master status page')
19 if 'error' in status or status == 'started':44 self.assertIn('Welcome to the Buildbot', page)
20 break
21 time.sleep(0.1)
22 self.assertEqual(unit_info('buildbot-master', 'state'), 'started')
23 finally:
24 juju('destroy-service', 'buildbot-master')
2545
2646
27if __name__ == '__main__':47if __name__ == '__main__':
2848
=== added file 'tests/test.cfg'
--- tests/test.cfg 1970-01-01 00:00:00 +0000
+++ tests/test.cfg 2012-02-08 22:09:17 +0000
@@ -0,0 +1,118 @@
1# -*- python -*-
2# ex: set syntax=python:
3
4# This is a sample buildmaster config file. It must be installed as
5# 'master.cfg' in your buildmaster's base directory.
6
7# This is the dictionary that the buildmaster pays attention to. We also use
8# a shorter alias to save typing.
9c = BuildmasterConfig = {}
10
11####### BUILDSLAVES
12
13# The 'slaves' list defines the set of recognized buildslaves. Each element is
14# a BuildSlave object, specifying a username and password. The same username and
15# password must be configured on the slave.
16from buildbot.buildslave import BuildSlave
17c['slaves'] = []
18
19# 'slavePortnum' defines the TCP port to listen on for connections from slaves.
20# This must match the value configured into the buildslaves (with their
21# --master option)
22c['slavePortnum'] = 9989
23
24####### CHANGESOURCES
25
26# the 'change_source' setting tells the buildmaster how it should find out
27# about source code changes. Here we point to the buildbot clone of pyflakes.
28
29from buildbot.changes.gitpoller import GitPoller
30c['change_source'] = GitPoller(
31 'git://github.com/buildbot/pyflakes.git',
32 branch='master', pollinterval=1200)
33
34####### SCHEDULERS
35
36# Configure the Schedulers, which decide how to react to incoming changes. In this
37# case, just kick off a 'runtests' build
38
39from buildbot.scheduler import Scheduler
40c['schedulers'] = []
41c['schedulers'].append(Scheduler(name="all", branch=None,
42 treeStableTimer=None,
43 builderNames=["runtests"]))
44
45####### BUILDERS
46
47# The 'builders' list defines the Builders, which tell Buildbot how to perform a build:
48# what steps, and which slaves can execute them. Note that any particular build will
49# only take place on one slave.
50
51from buildbot.process.factory import BuildFactory
52from buildbot.steps.source import Git
53from buildbot.steps.shell import ShellCommand
54
55factory = BuildFactory()
56# check out the source
57factory.addStep(Git(repourl='git://github.com/buildbot/pyflakes.git', mode='copy'))
58# run the tests (note that this will require that 'trial' is installed)
59factory.addStep(ShellCommand(command=["trial", "pyflakes"]))
60
61from buildbot.config import BuilderConfig
62
63c['builders'] = [
64 BuilderConfig(name="runtests",
65 # Buildbot enforces that the slavenames list must not be empty. Our
66 # wrapper will remove the empty string and replace it with proper values.
67 slavenames=[''],
68 factory=factory),
69 ]
70
71####### STATUS TARGETS
72
73# 'status' is a list of Status Targets. The results of each build will be
74# pushed to these targets. buildbot/status/*.py has a variety to choose from,
75# including web pages, email senders, and IRC bots.
76
77c['status'] = []
78
79from buildbot.status import html
80from buildbot.status.web import auth, authz
81authz_cfg=authz.Authz(
82 # change any of these to True to enable; see the manual for more
83 # options
84 gracefulShutdown = False,
85 forceBuild = True, # use this to test your slave once it is set up
86 forceAllBuilds = False,
87 pingBuilder = False,
88 stopBuild = False,
89 stopAllBuilds = False,
90 cancelPendingBuild = False,
91)
92c['status'].append(html.WebStatus(http_port=8010, authz=authz_cfg))
93
94####### PROJECT IDENTITY
95
96# the 'projectName' string will be used to describe the project that this
97# buildbot is working on. For example, it is used as the title of the
98# waterfall HTML page. The 'projectURL' string will be used to provide a link
99# from buildbot HTML pages to your project's home page.
100
101c['projectName'] = "Pyflakes"
102c['projectURL'] = "http://divmod.org/trac/wiki/DivmodPyflakes"
103
104# the 'buildbotURL' string should point to the location where the buildbot's
105# internal web server (usually the html.WebStatus page) is visible. This
106# typically uses the port number set in the Waterfall 'status' entry, but
107# with an externally-visible host name which the buildbot cannot figure out
108# without some help.
109
110c['buildbotURL'] = "http://localhost:8010/"
111
112####### DB URL
113
114# This specifies what database buildbot uses to store change and scheduler
115# state. You can leave this at its default for all but the largest
116# installations.
117c['db_url'] = "sqlite:///state.sqlite"
118

Subscribers

People subscribed via source and target branches

to all changes: