launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #00738
[Merge] lp:~jml/launchpad/launchpad-tester into lp:launchpad/devel
Jonathan Lange has proposed merging lp:~jml/launchpad/launchpad-tester into lp:launchpad/devel with lp:~jml/launchpad/subject-and-attachment as a prerequisite.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
This branch only makes one small behaviour change -- it flushes the summary
log more regularly so that the "Summary" on an ec2 test web server is always
up-to-date.
More interestingly, it adds tests for LaunchpadTester.
I was going to change the behaviour more significantly, but every time I
did I felt uncomfortable and uncertain.
--
https://code.launchpad.net/~jml/launchpad/launchpad-tester/+merge/33564
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jml/launchpad/launchpad-tester into lp:launchpad/devel.
=== modified file 'lib/devscripts/ec2test/remote.py'
--- lib/devscripts/ec2test/remote.py 2010-08-24 17:51:03 +0000
+++ lib/devscripts/ec2test/remote.py 2010-08-24 17:51:06 +0000
@@ -213,8 +213,6 @@
class LaunchpadTester:
"""Runs Launchpad tests and gathers their results in a useful way."""
- # XXX: JonathanLange 2010-08-17: LaunchpadTester needs tests.
-
def __init__(self, logger, test_directory, test_options=()):
"""Construct a TestOnMergeRunner.
@@ -236,9 +234,25 @@
Subclasses must provide their own implementation of this method.
"""
- command = ['make', 'check', 'TESTOPTS="%s"' % self._test_options]
+ command = ['make', 'check']
+ if self._test_options:
+ command.append('TESTOPTS="%s"' % self._test_options)
return command
+ def _spawn_test_process(self):
+ """Actually run the tests.
+
+ :return: A `subprocess.Popen` object for the test run.
+ """
+ call = self.build_test_command()
+ self._logger.write_line("Running %s" % (call,))
+ # bufsize=0 means do not buffer any of the output. We want to
+ # display the test output as soon as it is generated.
+ return subprocess.Popen(
+ call, bufsize=0,
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
+ cwd=self._test_directory)
+
def test(self):
"""Run the tests, log the results.
@@ -247,20 +261,9 @@
the user the test results.
"""
self._logger.prepare()
-
- call = self.build_test_command()
-
try:
- self._logger.write_line("Running %s" % (call,))
- # XXX: JonathanLange 2010-08-18: bufsize=-1 implies "use the
- # system buffering", when we actually want no buffering at all.
- popen = subprocess.Popen(
- call, bufsize=-1,
- stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
- cwd=self._test_directory)
-
+ popen = self._spawn_test_process()
self._gather_test_output(popen.stdout, self._logger)
-
exit_status = popen.wait()
except:
self._logger.error_in_testrunner(sys.exc_info())
@@ -277,6 +280,7 @@
for line in input_stream:
subunit_server.lineReceived(line)
logger.got_line(line)
+ summary_stream.flush()
class Request:
=== modified file 'lib/devscripts/ec2test/tests/test_remote.py'
--- lib/devscripts/ec2test/tests/test_remote.py 2010-08-24 17:51:03 +0000
+++ lib/devscripts/ec2test/tests/test_remote.py 2010-08-24 17:51:06 +0000
@@ -29,6 +29,7 @@
FlagFallStream,
gunzip_data,
gzip_data,
+ LaunchpadTester,
remove_pidfile,
Request,
SummaryResult,
@@ -37,6 +38,69 @@
)
+class RequestHelpers:
+
+ def patch(self, obj, name, value):
+ orig = getattr(obj, name)
+ setattr(obj, name, value)
+ self.addCleanup(setattr, obj, name, orig)
+ return orig
+
+ def make_trunk(self, parent_url='http://example.com/bzr/trunk'):
+ """Make a trunk branch suitable for use with `Request`.
+
+ `Request` expects to be given a path to a working tree that has a
+ branch with a configured parent URL, so this helper returns such a
+ working tree.
+ """
+ nick = parent_url.strip('/').split('/')[-1]
+ tree = self.make_branch_and_tree(nick)
+ tree.branch.set_parent(parent_url)
+ return tree
+
+ def make_request(self, branch_url=None, revno=None,
+ trunk=None, sourcecode_path=None,
+ emails=None, pqm_message=None):
+ """Make a request to test, specifying only things we care about.
+
+ Note that the returned request object will not ever send email, but
+ will instead log "sent" emails to `request.emails_sent`.
+ """
+ if trunk is None:
+ trunk = self.make_trunk()
+ if sourcecode_path is None:
+ sourcecode_path = self.make_sourcecode(
+ [('a', 'http://example.com/bzr/a', 2),
+ ('b', 'http://example.com/bzr/b', 3),
+ ('c', 'http://example.com/bzr/c', 5)])
+ request = Request(
+ branch_url, revno, trunk.basedir, sourcecode_path, emails,
+ pqm_message)
+ request.emails_sent = []
+ request._send_email = request.emails_sent.append
+ return request
+
+ def make_sourcecode(self, branches):
+ """Make a sourcecode directory with sample branches.
+
+ :param branches: A list of (name, parent_url, revno) tuples.
+ :return: The path to the sourcecode directory.
+ """
+ self.build_tree(['sourcecode/'])
+ for name, parent_url, revno in branches:
+ tree = self.make_branch_and_tree('sourcecode/%s' % (name,))
+ tree.branch.set_parent(parent_url)
+ for i in range(revno):
+ tree.commit(message=str(i))
+ return 'sourcecode/'
+
+ def make_logger(self, request=None, echo_to_stdout=False):
+ if request is None:
+ request = self.make_request()
+ return WebTestLogger(
+ 'full.log', 'summary.log', 'index.html', request, echo_to_stdout)
+
+
class TestFlagFallStream(TestCase):
"""Tests for `FlagFallStream`."""
@@ -122,6 +186,104 @@
self.assertEqual(1, len(flush_calls))
+class FakePopen:
+ """Fake Popen object so we don't have to spawn processes in tests."""
+
+ def __init__(self, output, exit_status):
+ self.stdout = StringIO(output)
+ self._exit_status = exit_status
+
+ def wait(self):
+ return self._exit_status
+
+
+class TestLaunchpadTester(TestCaseWithTransport, RequestHelpers):
+
+ def make_tester(self, logger=None, test_directory=None, test_options=()):
+ if not logger:
+ logger = self.make_logger()
+ if not test_directory:
+ test_directory = 'unspecified-test-directory'
+ return LaunchpadTester(logger, test_directory, test_options)
+
+ def test_build_test_command_no_options(self):
+ # The LaunchpadTester runs "make check" if given no options.
+ tester = self.make_tester()
+ command = tester.build_test_command()
+ self.assertEqual(['make', 'check'], command)
+
+ def test_build_test_command_options(self):
+ # The LaunchpadTester runs 'make check TESTOPTIONS="<options>"' if
+ # given options.
+ tester = self.make_tester(test_options=('-vvv', '--subunit'))
+ command = tester.build_test_command()
+ self.assertEqual(
+ ['make', 'check', 'TESTOPTS="-vvv --subunit"'], command)
+
+ def test_spawn_test_process(self):
+ # _spawn_test_process uses subprocess.Popen to run the command
+ # returned by build_test_command. stdout & stderr are piped together,
+ # the cwd is the test directory specified in the constructor, and the
+ # bufsize is zore, meaning "don't buffer".
+ popen_calls = []
+ self.patch(
+ subprocess, 'Popen',
+ lambda *args, **kwargs: popen_calls.append((args, kwargs)))
+ tester = self.make_tester(test_directory='test-directory')
+ tester._spawn_test_process()
+ self.assertEqual(
+ [((tester.build_test_command(),),
+ {'bufsize': 0,
+ 'stdout': subprocess.PIPE,
+ 'stderr': subprocess.STDOUT,
+ 'cwd': 'test-directory'})], popen_calls)
+
+ def test_running_test(self):
+ # LaunchpadTester.test() runs the test command, and then calls
+ # got_result with the result. This test is more of a smoke test to
+ # make sure that everything integrates well.
+ message = {'Subject': "One Crowded Hour"}
+ request = self.make_request(pqm_message=message)
+ logger = self.make_logger(request=request)
+ tester = self.make_tester(logger=logger)
+ output = "test output\n"
+ tester._spawn_test_process = lambda: FakePopen(output, 0)
+ tester.test()
+ # Message being sent implies got_result thought it got a success.
+ self.assertEqual([message], request.emails_sent)
+
+ def test_error_in_testrunner(self):
+ # Any exception is raised within LaunchpadTester.test() is an error in
+ # the testrunner. When we detect these, we do three things:
+ # 1. Log the error to the logger using error_in_testrunner
+ # 2. Call got_result with a False value, indicating test suite
+ # failure.
+ # 3. Re-raise the error. In the script, this triggers an email.
+ message = {'Subject': "One Crowded Hour"}
+ request = self.make_request(pqm_message=message)
+ logger = self.make_logger(request=request)
+ tester = self.make_tester(logger=logger)
+ # Break the test runner deliberately. In production, this is more
+ # likely to be a system error than a programming error.
+ tester._spawn_test_process = lambda: 1/0
+ self.assertRaises(ZeroDivisionError, tester.test)
+ # Message not being sent implies got_result thought it got a failure.
+ self.assertEqual([], request.emails_sent)
+ self.assertIn("ERROR IN TESTRUNNER", logger.get_summary_contents())
+ self.assertIn("ZeroDivisionError", logger.get_summary_contents())
+
+ def test_nonzero_exit_code(self):
+ message = {'Subject': "One Crowded Hour"}
+ request = self.make_request(pqm_message=message)
+ logger = self.make_logger(request=request)
+ tester = self.make_tester(logger=logger)
+ output = "test output\n"
+ tester._spawn_test_process = lambda: FakePopen(output, 10)
+ tester.test()
+ # Message not being sent implies got_result thought it got a failure.
+ self.assertEqual([], request.emails_sent)
+
+
class TestPidfileHelpers(TestCase):
"""Tests for `write_pidfile` and `remove_pidfile`."""
@@ -163,63 +325,6 @@
self.assertEqual(data, gunzip_data(compressed))
-class RequestHelpers:
-
- def make_trunk(self, parent_url='http://example.com/bzr/trunk'):
- """Make a trunk branch suitable for use with `Request`.
-
- `Request` expects to be given a path to a working tree that has a
- branch with a configured parent URL, so this helper returns such a
- working tree.
- """
- nick = parent_url.strip('/').split('/')[-1]
- tree = self.make_branch_and_tree(nick)
- tree.branch.set_parent(parent_url)
- return tree
-
- def make_request(self, branch_url=None, revno=None,
- trunk=None, sourcecode_path=None,
- emails=None, pqm_message=None):
- """Make a request to test, specifying only things we care about.
-
- Note that the returned request object will not ever send email, but
- will instead log "sent" emails to `request.emails_sent`.
- """
- if trunk is None:
- trunk = self.make_trunk()
- if sourcecode_path is None:
- sourcecode_path = self.make_sourcecode(
- [('a', 'http://example.com/bzr/a', 2),
- ('b', 'http://example.com/bzr/b', 3),
- ('c', 'http://example.com/bzr/c', 5)])
- request = Request(
- branch_url, revno, trunk.basedir, sourcecode_path, emails,
- pqm_message)
- request.emails_sent = []
- request._send_email = request.emails_sent.append
- return request
-
- def make_sourcecode(self, branches):
- """Make a sourcecode directory with sample branches.
-
- :param branches: A list of (name, parent_url, revno) tuples.
- :return: The path to the sourcecode directory.
- """
- self.build_tree(['sourcecode/'])
- for name, parent_url, revno in branches:
- tree = self.make_branch_and_tree('sourcecode/%s' % (name,))
- tree.branch.set_parent(parent_url)
- for i in range(revno):
- tree.commit(message=str(i))
- return 'sourcecode/'
-
- def make_logger(self, request=None, echo_to_stdout=False):
- if request is None:
- request = self.make_request()
- return WebTestLogger(
- 'full.log', 'summary.log', 'index.html', request, echo_to_stdout)
-
-
class TestRequest(TestCaseWithTransport, RequestHelpers):
"""Tests for `Request`."""
@@ -437,12 +542,6 @@
class TestWebTestLogger(TestCaseWithTransport, RequestHelpers):
- def patch(self, obj, name, value):
- orig = getattr(obj, name)
- setattr(obj, name, value)
- self.addCleanup(setattr, obj, name, orig)
- return orig
-
def test_make_in_directory(self):
# WebTestLogger.make_in_directory constructs a logger that writes to a
# bunch of specific files in a directory.
Follow ups