yellow team mailing list archive
-
yellow team
-
Mailing list archive
-
Message #00375
[Merge] lp:~benji/charms/oneiric/buildbot-master/more-tests into lp:~yellow/charms/oneiric/buildbot-master/trunk
Benji York has proposed merging lp:~benji/charms/oneiric/buildbot-master/more-tests into lp:~yellow/charms/oneiric/buildbot-master/trunk.
Requested reviews:
Launchpad Yellow Squad (yellow)
For more details, see:
https://code.launchpad.net/~benji/charms/oneiric/buildbot-master/more-tests/+merge/92123
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
--
https://code.launchpad.net/~benji/charms/oneiric/buildbot-master/more-tests/+merge/92123
Your team Launchpad Yellow Squad is requested to review the proposed merge of lp:~benji/charms/oneiric/buildbot-master/more-tests into lp:~yellow/charms/oneiric/buildbot-master/trunk.
=== modified file 'HACKING.txt'
--- HACKING.txt 2012-02-02 18:35:04 +0000
+++ HACKING.txt 2012-02-08 20:09:18 +0000
@@ -14,7 +14,11 @@
real juju executable, naming it "juju". Edit the CHARM_TEST_REPO variable
therein to reflect the location of the charm repo from step 1.
-3) Run the tests: RESOLVE_TEST_CHARMS=1 tests/buildbot-master.test
+3) Bootstrap the juju environment if not already done:
+
+ juju bootstrap
+
+4) Run the tests: RESOLVE_TEST_CHARMS=1 tests/buildbot-master.test
Running the charm helper tests
=== modified file 'hooks/buildbot-relation-changed'
--- hooks/buildbot-relation-changed 2012-02-07 17:30:54 +0000
+++ hooks/buildbot-relation-changed 2012-02-08 20:09:18 +0000
@@ -5,6 +5,8 @@
from helpers import (
log,
+ log_entry,
+ log_exit,
relation_get,
relation_set,
)
@@ -48,5 +50,8 @@
if __name__ == '__main__':
- log('BUILDBOT-RELATION-CHANGED HOOK:')
- main()
+ log_entry()
+ try:
+ main()
+ finally:
+ log_exit()
=== modified file 'hooks/config-changed'
--- hooks/config-changed 2012-02-08 09:57:46 +0000
+++ hooks/config-changed 2012-02-08 20:09:18 +0000
@@ -18,6 +18,8 @@
get_config,
install_extra_repository,
log,
+ log_entry,
+ log_exit,
run,
)
from local import (
@@ -65,6 +67,7 @@
# Add a new repository if it was just added.
added_or_changed = diff.added_or_changed
+
if extra_repo and 'extra-repository' in added_or_changed:
install_extra_repository(extra_repo)
restart_required = True
@@ -124,6 +127,7 @@
def initialize_buildbot(config):
# Initialize the buildbot directory and (re)start buildbot.
+ log("Initializing buildbot")
buildbot_dir = config.get('installdir')
master_cfg_path = os.path.join(buildbot_dir, 'master.cfg')
shutil.move(master_cfg_path, master_cfg_path + '.original')
@@ -158,9 +162,8 @@
if not os.path.exists(buildbot_dir):
os.makedirs(buildbot_dir)
- restart_required = (
- handle_config_changes(config, diff) or
- configure_buildbot(config, diff))
+ restart_required = handle_config_changes(config, diff)
+ restart_required |= configure_buildbot(config, diff)
master_cfg_path = os.path.join(buildbot_dir, 'master.cfg')
if restart_required and os.path.exists(master_cfg_path):
@@ -172,5 +175,8 @@
if __name__ == '__main__':
- log('CONFIG-CHANGED HOOK:')
- main()
+ log_entry()
+ try:
+ main()
+ finally:
+ log_exit()
=== modified file 'hooks/helpers.py'
--- hooks/helpers.py 2012-02-07 14:37:13 +0000
+++ hooks/helpers.py 2012-02-08 20:09:18 +0000
@@ -13,21 +13,39 @@
'grep',
'install_extra_repository',
'log',
+ 'log_entry',
+ 'log_exit',
'run',
'relation_get',
'relation_set',
'unit_info',
]
+from textwrap import dedent
import json
import os
import re
import subprocess
import sys
-from textwrap import dedent
+import time
import yaml
+def run(*args):
+ """Run the command with the given arguments.
+
+ The first argument is the path to the command to run, subsequent arguments
+ are command-line arguments to be passed.
+ """
+ process = subprocess.Popen(args, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE, close_fds=True)
+ stdout, stderr = process.communicate()
+ if process.returncode:
+ raise subprocess.CalledProcessError(
+ process.returncode, repr(args), output=stdout+stderr)
+ return stdout
+
+
def command(*base_args):
"""Return a callable that will run the given command with any arguments.
@@ -48,7 +66,7 @@
"""
def callable_command(*args):
all_args = base_args + args
- return subprocess.check_output(all_args, shell=False)
+ return run(*all_args)
return callable_command
@@ -57,11 +75,6 @@
apt_get_install = command('apt-get', 'install', '-y', '--force-yes')
-def run(*args):
- log('run: ' + str(args))
- return subprocess.check_output(args, shell=False)
-
-
def get_config():
config_get = command('config-get', '--format=json')
return json.loads(config_get())
@@ -72,9 +85,9 @@
run('apt-add-repository', extra_repository)
run('apt-get', 'update')
except subprocess.CalledProcessError as e:
- log("Error adding repository %s" % extra_repository)
+ log('Error adding repository: ' + extra_repository)
log(e)
- sys.exit(-1)
+ raise
def relation_get(*args):
@@ -108,6 +121,18 @@
return line.split('=')[1].strip('"\' ')
+def script_name():
+ return os.path.basename(sys.argv[0])
+
+
+def log_entry():
+ log("<-- Entering {}".format(script_name()))
+
+
+def log_exit():
+ log("--> Exiting {}".format(script_name()))
+
+
class DictDiffer:
"""
Calculate the difference between two dictionaries as:
@@ -158,6 +183,7 @@
new[k] = self.current_dict.get(k)
old[k] = self.past_dict.get(k)
return "%s -> %s" % (old, new)
+
def __str__(self):
if self.modified:
s = dedent("""\
@@ -174,6 +200,28 @@
return s
+def unit_info(service_name, item_name, data=None):
+ if data is None:
+ data = yaml.safe_load(run('juju', 'status'))
+ services = data['services'][service_name]
+ units = services['units']
+ item = units.items()[0][1][item_name]
+ return item
+
+
+def wait_for_unit(service_name, timeout=120):
+ start_time = time.time()
+ while True:
+ status = unit_info(service_name, 'state')
+ if 'error' in status or status == 'started':
+ break
+ if time.time() - start_time >= timeout:
+ raise RuntimeError('timeout waiting for service to start')
+ time.sleep(0.1)
+ state = unit_info('buildbot-master', 'state')
+ if state != 'started':
+ raise RuntimeError('unit did not start, state: ' + state)
+
class Serializer:
"""Handle JSON (de)serialization."""
=== modified file 'hooks/install'
--- hooks/install 2012-02-07 17:30:54 +0000
+++ hooks/install 2012-02-08 20:09:18 +0000
@@ -11,6 +11,8 @@
apt_get_install,
get_config,
log,
+ log_entry,
+ log_exit,
run,
)
from local import (
@@ -44,5 +46,8 @@
if __name__ == '__main__':
- log('INSTALL HOOK:')
- main()
+ log_entry()
+ try:
+ main()
+ finally:
+ log_exit()
=== modified file 'hooks/local.py'
--- hooks/local.py 2012-02-07 14:34:40 +0000
+++ hooks/local.py 2012-02-08 20:09:18 +0000
@@ -11,6 +11,7 @@
'config_json',
'create_slave',
'generate_string',
+ 'HTTP_PORT_PROTOCOL',
'slave_json',
]
@@ -26,6 +27,9 @@
)
+HTTP_PORT_PROTOCOL = '8010/TCP'
+
+
def _get_buildbot_dir():
config = get_config()
return config.get('installdir')
=== modified file 'hooks/start'
--- hooks/start 2012-02-07 13:29:08 +0000
+++ hooks/start 2012-02-08 20:09:18 +0000
@@ -5,15 +5,21 @@
from helpers import (
log,
+ log_entry,
+ log_exit,
run,
)
+from local import HTTP_PORT_PROTOCOL
def main():
# Actually starting the buildbot happens in config-changed.
- run('open-port', '8010/TCP')
+ run('open-port', HTTP_PORT_PROTOCOL)
if __name__ == '__main__':
- log('START HOOK:')
- main()
+ log_entry()
+ try:
+ main()
+ finally:
+ log_exit()
=== modified file 'hooks/stop'
--- hooks/stop 2012-02-07 13:29:08 +0000
+++ hooks/stop 2012-02-08 20:09:18 +0000
@@ -6,18 +6,24 @@
from helpers import (
get_config,
log,
+ log_entry,
+ log_exit,
run,
)
+from local import HTTP_PORT_PROTOCOL
def main():
config = get_config()
log('Stopping buildbot in {}'.format(config['installdir']))
- run('close-port', '8010/TCP')
+ run('close-port', HTTP_PORT_PROTOCOL)
log('Finished stopping buildbot')
if __name__ == '__main__':
- log('STOP HOOK:')
- main()
+ log_entry()
+ try:
+ main()
+ finally:
+ log_exit()
=== modified file 'hooks/tests.py'
--- hooks/tests.py 2012-02-07 14:37:13 +0000
+++ hooks/tests.py 2012-02-08 20:09:18 +0000
@@ -3,11 +3,36 @@
from subprocess import CalledProcessError
from helpers import (
+ DictDiffer,
command,
- DictDiffer,
+ run,
unit_info,
)
+from subprocess import CalledProcessError
+
+class TestRun(unittest.TestCase):
+
+ def testSimpleCommand(self):
+ # Running a simple command (ls) works and running the command
+ # produces a string.
+ self.assertIsInstance(run('/bin/ls'), str)
+
+ def testStdoutReturned(self):
+ # Running a simple command (ls) works and running the command
+ # produces a string.
+ self.assertIn('Usage:', run('/bin/ls', '--help'))
+
+ def testSimpleCommand(self):
+ # If an error ocurrs a CalledProcessError is raised with the return
+ # code, command executed, and the output of the command.
+ with self.assertRaises(CalledProcessError) as info:
+ run('ls', '--not a valid switch')
+ exception = info.exception
+ self.assertEqual(2, exception.returncode)
+ self.assertEqual("('ls', '--not a valid switch')", exception.cmd)
+ self.assertIn('unrecognized option', exception.output)
+
class TestCommand(unittest.TestCase):
=== modified file 'tests/buildbot-master.test'
--- tests/buildbot-master.test 2012-02-07 12:35:06 +0000
+++ tests/buildbot-master.test 2012-02-08 20:09:18 +0000
@@ -1,27 +1,42 @@
#!/usr/bin/python
-
# Copyright 2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-from helpers import command, run, unit_info
+from helpers import command, run, unit_info, wait_for_unit
+import base64
+import os.path
import time
import unittest
+import urllib2
juju = command('juju')
class testCharm(unittest.TestCase):
+ def tearDown(self):
+ juju('destroy-service', 'buildbot-master')
+
def testDeploy(self):
+ # See if the charm will actuall deploy correctly. This test may be
+ # made redunant by standard charm tests run by some future centralized
+ # charm repository.
+ juju('deploy', 'buildbot-master')
+ wait_for_unit('buildbot-master')
+ self.assertEqual(unit_info('buildbot-master', 'state'), 'started')
+
+ def testPortOpened(self):
+ juju('deploy', 'buildbot-master')
+ juju('set', 'buildbot-master', 'extra-packages=git')
+ config_path = os.path.join(os.path.dirname(__file__), 'test.cfg')
+ config = base64.b64encode(open(config_path).read())
+ juju('set', 'buildbot-master', 'config-file='+config)
+ wait_for_unit('buildbot-master')
+ addr = unit_info('buildbot-master', 'public-address')
try:
- juju('deploy', 'buildbot-master')
- while True:
- status = unit_info('buildbot-master', 'state')
- if 'error' in status or status == 'started':
- break
- time.sleep(0.1)
- self.assertEqual(unit_info('buildbot-master', 'state'), 'started')
- finally:
- juju('destroy-service', 'buildbot-master')
+ page = urllib2.urlopen('http://{}:8010'.format(addr)).read()
+ except urllib2.URLError:
+ self.fail('could not fetch buildbot master status page')
+ self.assertIn('Welcome to the Buildbot', page)
if __name__ == '__main__':
=== added file 'tests/test.cfg'
--- tests/test.cfg 1970-01-01 00:00:00 +0000
+++ tests/test.cfg 2012-02-08 20:09:18 +0000
@@ -0,0 +1,118 @@
+# -*- python -*-
+# ex: set syntax=python:
+
+# This is a sample buildmaster config file. It must be installed as
+# 'master.cfg' in your buildmaster's base directory.
+
+# This is the dictionary that the buildmaster pays attention to. We also use
+# a shorter alias to save typing.
+c = BuildmasterConfig = {}
+
+####### BUILDSLAVES
+
+# The 'slaves' list defines the set of recognized buildslaves. Each element is
+# a BuildSlave object, specifying a username and password. The same username and
+# password must be configured on the slave.
+from buildbot.buildslave import BuildSlave
+c['slaves'] = []
+
+# 'slavePortnum' defines the TCP port to listen on for connections from slaves.
+# This must match the value configured into the buildslaves (with their
+# --master option)
+c['slavePortnum'] = 9989
+
+####### CHANGESOURCES
+
+# the 'change_source' setting tells the buildmaster how it should find out
+# about source code changes. Here we point to the buildbot clone of pyflakes.
+
+from buildbot.changes.gitpoller import GitPoller
+c['change_source'] = GitPoller(
+ 'git://github.com/buildbot/pyflakes.git',
+ branch='master', pollinterval=1200)
+
+####### SCHEDULERS
+
+# Configure the Schedulers, which decide how to react to incoming changes. In this
+# case, just kick off a 'runtests' build
+
+from buildbot.scheduler import Scheduler
+c['schedulers'] = []
+c['schedulers'].append(Scheduler(name="all", branch=None,
+ treeStableTimer=None,
+ builderNames=["runtests"]))
+
+####### BUILDERS
+
+# The 'builders' list defines the Builders, which tell Buildbot how to perform a build:
+# what steps, and which slaves can execute them. Note that any particular build will
+# only take place on one slave.
+
+from buildbot.process.factory import BuildFactory
+from buildbot.steps.source import Git
+from buildbot.steps.shell import ShellCommand
+
+factory = BuildFactory()
+# check out the source
+factory.addStep(Git(repourl='git://github.com/buildbot/pyflakes.git', mode='copy'))
+# run the tests (note that this will require that 'trial' is installed)
+factory.addStep(ShellCommand(command=["trial", "pyflakes"]))
+
+from buildbot.config import BuilderConfig
+
+c['builders'] = [
+ BuilderConfig(name="runtests",
+ # Buildbot enforces that the slavenames list must not be empty. Our
+ # wrapper will remove the empty string and replace it with proper values.
+ slavenames=[''],
+ factory=factory),
+ ]
+
+####### STATUS TARGETS
+
+# 'status' is a list of Status Targets. The results of each build will be
+# pushed to these targets. buildbot/status/*.py has a variety to choose from,
+# including web pages, email senders, and IRC bots.
+
+c['status'] = []
+
+from buildbot.status import html
+from buildbot.status.web import auth, authz
+authz_cfg=authz.Authz(
+ # change any of these to True to enable; see the manual for more
+ # options
+ gracefulShutdown = False,
+ forceBuild = True, # use this to test your slave once it is set up
+ forceAllBuilds = False,
+ pingBuilder = False,
+ stopBuild = False,
+ stopAllBuilds = False,
+ cancelPendingBuild = False,
+)
+c['status'].append(html.WebStatus(http_port=8010, authz=authz_cfg))
+
+####### PROJECT IDENTITY
+
+# the 'projectName' string will be used to describe the project that this
+# buildbot is working on. For example, it is used as the title of the
+# waterfall HTML page. The 'projectURL' string will be used to provide a link
+# from buildbot HTML pages to your project's home page.
+
+c['projectName'] = "Pyflakes"
+c['projectURL'] = "http://divmod.org/trac/wiki/DivmodPyflakes"
+
+# the 'buildbotURL' string should point to the location where the buildbot's
+# internal web server (usually the html.WebStatus page) is visible. This
+# typically uses the port number set in the Waterfall 'status' entry, but
+# with an externally-visible host name which the buildbot cannot figure out
+# without some help.
+
+c['buildbotURL'] = "http://localhost:8010/"
+
+####### DB URL
+
+# This specifies what database buildbot uses to store change and scheduler
+# state. You can leave this at its default for all but the largest
+# installations.
+c['db_url'] = "sqlite:///state.sqlite"
+
Follow ups