diff --git a/test/assertions.py b/test/assertions.py index 62ecc4c..4e26a4a 100755 --- a/test/assertions.py +++ b/test/assertions.py @@ -1,45 +1,45 @@ #!/usr/bin/python import re class BoothAssertions: def configFileMissingMyIP(self, config_file=None, lock_file=None): (pid, ret, stdout, stderr, runner) = \ self.run_booth(config_file=config_file, lock_file=lock_file, - expected_exitcode=1) + expected_exitcode=1, expected_daemon=False) expected_error = "ERROR: can't find myself in config file" self.assertRegexpMatches(self.read_log(), expected_error) def assertLockFileError(self, config_file=None, config_text=None, lock_file=True, args=[]): (pid, ret, stdout, stderr, runner) = \ self.run_booth(config_text=config_text, config_file=config_file, lock_file=lock_file, args=args, expected_exitcode=1) expected_error = 'lockfile open error %s: Permission denied' % runner.lock_file_used() self.assertRegexpMatches(self.read_log(), expected_error) ###################################################################### # backported from 2.7 just in case we're running on an older Python def assertRegexpMatches(self, text, expected_regexp, msg=None): """Fail the test unless the text matches the regular expression.""" if isinstance(expected_regexp, basestring): expected_regexp = re.compile(expected_regexp) if not expected_regexp.search(text): msg = msg or "Regexp didn't match" msg = '%s: %r not found in %r' % (msg, expected_regexp.pattern, text) raise self.failureException(msg) def assertNotRegexpMatches(self, text, unexpected_regexp, msg=None): """Fail the test if the text matches the regular expression.""" if isinstance(unexpected_regexp, basestring): unexpected_regexp = re.compile(unexpected_regexp) match = unexpected_regexp.search(text) if match: msg = msg or "Regexp matched" msg = '%s: %r matches %r in %r' % (msg, text[match.start():match.end()], unexpected_regexp.pattern, text) raise self.failureException(msg) ###################################################################### diff --git a/test/boothtestenv.py b/test/boothtestenv.py index eec3ffa..89a484a 100755 --- a/test/boothtestenv.py +++ b/test/boothtestenv.py @@ -1,73 +1,71 @@ #!/usr/bin/python import os import subprocess import time import tempfile import unittest from assertions import BoothAssertions from boothrunner import BoothRunner class BoothTestEnvironment(unittest.TestCase, BoothAssertions): test_src_path = os.path.abspath(os.path.dirname(__file__)) dist_path = os.path.join(test_src_path, '..' ) src_path = os.path.join(dist_path, 'src' ) boothd_path = os.path.join(src_path, 'boothd') conf_path = os.path.join(dist_path, 'conf' ) example_config_path = os.path.join(conf_path, 'booth.conf.example') def setUp(self): if not self._testMethodName.startswith('test_'): raise RuntimeError, "unexpected test method name: " + self._testMethodName self.test_name = self._testMethodName[5:] self.test_path = os.path.join(self.test_run_path, self.test_name) os.makedirs(self.test_path) self.ensure_boothd_not_running() def ensure_boothd_not_running(self): # Need to redirect STDERR in case we're not root, in which # case netstat's -p option causes a warning. However we only # want to kill boothd processes which we own; -p will list the # pid for those and only those, which is exactly what we want # here. subprocess.call("netstat -tpln 2>&1 | perl -lne 'm,LISTEN\s+(\d+)/boothd, and kill 15, $1'", shell=True) def get_tempfile(self, identity): tf = tempfile.NamedTemporaryFile( prefix='%s.%d.' % (identity, time.time()), dir=self.test_path, delete=False ) return tf.name def init_log(self): self.log_file = self.get_tempfile('log') os.putenv('HA_debugfile', self.log_file) # See cluster-glue/lib/clplumbing/cl_log.c def read_log(self): if not os.path.exists(self.log_file): return '' l = open(self.log_file) msgs = ''.join(l.readlines()) l.close() return msgs def check_return_code(self, pid, return_code, expected_exitcode): if return_code is None: print "pid %d still running" % pid if expected_exitcode is not None: self.fail("expected exit code %d, not long-running process" % expected_exitcode) else: print "pid %d exited with code %d" % (pid, return_code) - msg = "should exit with code %s" % expected_exitcode - msg += "\nlog follows (see %s)" % self.log_file + if expected_exitcode is None: + msg = "should not exit" + else: + msg = "should exit with code %s" % expected_exitcode + msg += "\nLog follows (see %s)" % self.log_file msg += "\nN.B. expect mlockall/setscheduler errors when running tests non-root" msg += "\n-----------\n%s" % self.read_log() self.assertEqual(return_code, expected_exitcode, msg) - - def _test_buffer_overflow(self, expected_error, **args): - (pid, ret, stdout, stderr, runner) = \ - self.run_booth(expected_exitcode=1, **args) - self.assertRegexpMatches(stderr, expected_error) diff --git a/test/clientenv.py b/test/clientenv.py index e4f9f7d..fcd40fa 100755 --- a/test/clientenv.py +++ b/test/clientenv.py @@ -1,24 +1,29 @@ #!/usr/bin/python from boothtestenv import BoothTestEnvironment from boothrunner import BoothRunner class ClientTestEnvironment(BoothTestEnvironment): mode = 'client' def run_booth(self, config_text=None, config_file=None, lock_file=True, args=[], expected_exitcode=0, debug=False): ''' Runs boothd. Returns a (pid, return_code, stdout, stderr, runner) tuple, where return_code/stdout/stderr are None iff pid is still running. ''' self.init_log() runner = BoothRunner(self.boothd_path, self.mode, args) runner.show_args() (pid, return_code, stdout, stderr) = runner.run() self.check_return_code(pid, return_code, expected_exitcode) return (pid, return_code, stdout, stderr, runner) + + def _test_buffer_overflow(self, expected_error, **args): + (pid, ret, stdout, stderr, runner) = \ + self.run_booth(expected_exitcode=1, **args) + self.assertRegexpMatches(stderr, expected_error) diff --git a/test/serverenv.py b/test/serverenv.py index c53b23a..955ed1a 100755 --- a/test/serverenv.py +++ b/test/serverenv.py @@ -1,146 +1,203 @@ #!/usr/bin/python import os import re import time import unittest from assertions import BoothAssertions from boothrunner import BoothRunner from boothtestenv import BoothTestEnvironment from utils import get_IP class ServerTestEnvironment(BoothTestEnvironment): + ''' + boothd site/arbitrator will hang in setup phase while attempting to connect + to an unreachable peer during ticket_catchup(). In a test environment we don't + have any reachable peers. Fortunately, we can still successfully launch a + daemon by only listing our own IP in the config file. + ''' typical_config = """\ # This is like the config in the manual transport="UDP" port="6666" # Here's another comment -arbitrator="147.2.207.14" +#arbitrator="147.2.207.14" site="147.4.215.19" -site="147.18.2.1" +#site="147.18.2.1" ticket="ticketA" ticket="ticketB" """ - working_config = re.sub('site=".+"', 'site="%s"' % get_IP(), typical_config, 1) + site_re = re.compile('^site=".+"', re.MULTILINE) + working_config = re.sub(site_re, 'site="%s"' % get_IP(), typical_config, 1) - def run_booth(self, config_text=None, config_file=None, lock_file=True, args=[], - expected_exitcode=0, debug=False): + def run_booth(self, expected_exitcode, expected_daemon, + config_text=None, config_file=None, lock_file=True, + args=[], debug=False): ''' - Runs boothd. Defaults to using a temporary lock file and - the standard config file path. + Runs boothd. Defaults to using a temporary lock file and the + standard config file path. There are four possible types of + outcome: + + - boothd exits non-zero without launching a daemon (setup phase failed, + e.g. due to invalid configuration file) + - boothd exits zero after launching a daemon (successful operation) + - boothd does not exit (running in foreground / debug mode) + - boothd does not exit (setup phase hangs, e.g. while attempting + to connect to peer during ticket_catchup()) + + Arguments: + config_text + a string containing the contents of a configuration file to use + config_file + path to a configuration file to use + lock_file + False: don't pass a lockfile parameter to booth via -l + True: pass a temporary lockfile parameter to booth via -l + string: pass the given lockfile path to booth via -l + args + array of extra args to pass to booth + expected_exitcode + an integer, or False if booth is not expected to terminate + within the timeout + expected_daemon + True iff a daemon is expected to be launched (this includes + running the server in debug / foreground mode via -D; even + though in this case the server's not technically not a daemon, + we still want to treat it like one by checking the lockfile + before and after we kill it) + debug + True means pass the -D parameter Returns a (pid, return_code, stdout, stderr, runner) tuple, where return_code/stdout/stderr are None iff pid is still running. ''' + if expected_daemon and expected_exitcode is not None and expected_exitcode != 0: + raise RuntimeError, \ + "Shouldn't ever expect daemon to start and then failure" + + if not expected_daemon and expected_exitcode == 0: + raise RuntimeError, \ + "Shouldn't ever expect success without starting daemon" + self.init_log() runner = BoothRunner(self.boothd_path, self.mode, args) if config_text: config_file = self.write_config_file(config_text) if config_file: runner.set_config_file(config_file) if lock_file is True: lock_file = os.path.join(self.test_path, 'boothd-lock.pid') if lock_file: runner.set_lock_file(lock_file) if debug: runner.set_debug() runner.show_args() (pid, return_code, stdout, stderr) = runner.run() self.check_return_code(pid, return_code, expected_exitcode) - expected_daemon = expected_exitcode == 0 or expected_exitcode is None - got_daemon = return_code == 0 or return_code is None - - if got_daemon: + if expected_daemon: self.check_daemon_handling(runner, expected_daemon) + elif return_code is None: + # This isn't strictly necessary because we ensure no + # daemon is running from within test setUp(), but it's + # probably a good idea to tidy up after ourselves anyway. + self.kill_pid(pid) return (pid, return_code, stdout, stderr, runner) def write_config_file(self, config_text): config_file = self.get_tempfile('config') c = open(config_file, 'w') c.write(config_text) c.close() return config_file + def kill_pid(self, pid): + print "killing %d ..." % pid + os.kill(pid, 15) + print "killed" + def check_daemon_handling(self, runner, expected_daemon): ''' Check that the lock file contains a pid referring to a running daemon. Then kill the daemon, and ensure that the lock file vanishes (bnc#749763). ''' daemon_pid = self.get_daemon_pid_from_lock_file(runner.lock_file) err = "lock file should contain pid" if not expected_daemon: err += ", even though we didn't expect a daemon" self.assertTrue(daemon_pid is not None, err) daemon_running = self.is_pid_running_daemon(daemon_pid) err = "pid in lock file should referred to a running daemon" self.assertTrue(daemon_running, err) if daemon_running: - print "killing %s ..." % daemon_pid - os.kill(int(daemon_pid), 15) - print "killed" + self.kill_pid(int(daemon_pid)) time.sleep(1) daemon_pid = self.get_daemon_pid_from_lock_file(runner.lock_file) self.assertTrue(daemon_pid is not None, 'bnc#749763: lock file should vanish after daemon is killed') def get_daemon_pid_from_lock_file(self, lock_file): ''' Returns the pid contained in lock_file, or None if it doesn't exist. ''' if not os.path.exists(lock_file): print "%s does not exist" % lock_file return None l = open(lock_file) lines = l.readlines() l.close() self.assertEqual(len(lines), 1, "Lock file should contain one line") pid = lines[0].rstrip() print "lockfile contains: %s" % pid return pid def is_pid_running_daemon(self, pid): ''' Returns true iff the given pid refers to a running boothd process. ''' path = "/proc/%s" % pid pid_running = os.path.isdir(path) # print "======" # import subprocess # print subprocess.check_output(['lsof', '-p', pid]) # print subprocess.check_output(['ls', path]) # print subprocess.check_output(['cat', "/proc/%s/cmdline" % pid]) # print "======" if not pid_running: return False c = open("/proc/%s/cmdline" % pid) cmdline = "".join(c.readlines()) print cmdline c.close() if cmdline.find('boothd') == -1: print 'no boothd in cmdline:', cmdline return False # self.assertRegexpMatches( # cmdline, # 'boothd', # "lock file should refer to pid of running boothd" # ) return True + + def _test_buffer_overflow(self, expected_error, **args): + (pid, ret, stdout, stderr, runner) = \ + self.run_booth(expected_exitcode=1, expected_daemon=False, **args) + self.assertRegexpMatches(stderr, expected_error) diff --git a/test/servertests.py b/test/servertests.py index 9b8b55b..68cfdb5 100755 --- a/test/servertests.py +++ b/test/servertests.py @@ -1,129 +1,140 @@ #!/usr/bin/python import copy from pprint import pprint, pformat import re import string from serverenv import ServerTestEnvironment class ServerTests(ServerTestEnvironment): # We don't know enough about the build/test system to rely on the # existence, permissions, contents of the default config file. So # we can't predict (and hence test) how booth will behave when -c # is not specified. # # def test_no_args(self): # # If do_server() called lockfile() first then this would be # # the appropriate test: # #self.assertLockFileError(lock_file=False) # # # If do_server() called setup() first, and the default # # config file was readable non-root, then this would be the # # appropriate test: # self.configFileMissingMyIP(lock_file=False) # # def test_custom_lock_file(self): - # (pid, ret, stdout, stderr, runner) = self.run_booth(expected_exitcode=1) + # (pid, ret, stdout, stderr, runner) = \ + # self.run_booth(expected_exitcode=1, expected_daemon=False) # self.assertRegexpMatches( # stderr, # 'failed to open %s: ' % runner.config_file_used(), # 'should fail to read default config file' # ) def test_example_config(self): self.configFileMissingMyIP(config_file=self.example_config_path) def test_config_file_buffer_overflow(self): # https://bugzilla.novell.com/show_bug.cgi?id=750256 longfile = (string.lowercase * 5)[:127] expected_error = "'%s' exceeds maximum config file length" % longfile self._test_buffer_overflow(expected_error, config_file=longfile) def test_lock_file_buffer_overflow(self): # https://bugzilla.novell.com/show_bug.cgi?id=750256 longfile = (string.lowercase * 5)[:127] expected_error = "'%s' exceeds maximum lock file length" % longfile self._test_buffer_overflow(expected_error, lock_file=longfile) def test_working_config(self): (pid, ret, stdout, stderr, runner) = \ - self.run_booth(config_text=self.working_config) + self.run_booth(expected_exitcode=0, expected_daemon=True, + config_text=self.working_config) def test_missing_quotes(self): orig_lines = self.working_config.split("\n") for i in xrange(len(orig_lines)): new_lines = copy.copy(orig_lines) new_lines[i] = new_lines[i].replace('"', '') new_config = "\n".join(new_lines) - line_contains_IP = re.search('=.+\.', orig_lines[i]) + line_contains_IP = re.search('^\s*(site|arbitrator)=.*[0-9]\.', orig_lines[i]) if line_contains_IP: - # IP addresses need to be surrounded by quotes + # IP addresses need to be surrounded by quotes, + # so stripping them should cause it to fail expected_exitcode = 1 + expected_daemon = False else: expected_exitcode = 0 + expected_daemon = True (pid, ret, stdout, stderr, runner) = \ self.run_booth(config_text=new_config, - expected_exitcode=expected_exitcode) + expected_exitcode=expected_exitcode, + expected_daemon=expected_daemon) if line_contains_IP: self.assertRegexpMatches( self.read_log(), "ERROR: invalid config file format: unquoted '.'", 'IP addresses need to be quoted' ) def test_debug_mode(self): (pid, ret, stdout, stderr, runner) = \ self.run_booth(config_text=self.working_config, debug=True, - expected_exitcode=None) + expected_exitcode=None, expected_daemon=True) def test_missing_transport(self): config = re.sub('transport=.+\n', '', self.typical_config) (pid, ret, stdout, stderr, runner) = \ - self.run_booth(config_text=config, expected_exitcode=1) + self.run_booth(config_text=config, expected_exitcode=1, expected_daemon=False) self.assertRegexpMatches( self.read_log(), 'config file was missing transport line' ) def test_invalid_transport_protocol(self): config = re.sub('transport=.+', 'transport=SNEAKERNET', self.typical_config) (pid, ret, stdout, stderr, runner) = \ - self.run_booth(config_text=config, expected_exitcode=1) + self.run_booth(config_text=config, expected_exitcode=1, expected_daemon=False) self.assertRegexpMatches( self.read_log(), 'invalid transport protocol' ) def test_missing_final_newline(self): config = re.sub('\n$', '', self.working_config) (pid, ret, stdout, stderr, runner) = \ - self.run_booth(config_text=config) + self.run_booth(config_text=config, expected_exitcode=0, expected_daemon=True) def test_a_few_trailing_whitespaces(self): for ws in (' ', ' '): new_config = self.working_config.replace("\n", ws + "\n", 3) (pid, ret, stdout, stderr, runner) = \ self.run_booth(config_text=new_config, - expected_exitcode=0) + expected_exitcode=0, expected_daemon=True) def test_trailing_space_everywhere(self): for ws in (' ', ' '): new_config = self.working_config.replace("\n", ws + "\n") (pid, ret, stdout, stderr, runner) = \ self.run_booth(config_text=new_config, - expected_exitcode=0) + expected_exitcode=0, expected_daemon=True) def test_unquoted_space(self): for ticket in ('unquoted space', 'unquoted space man'): new_config = re.sub('ticket=.+', 'ticket=' + ticket, self.working_config, 1) (pid, ret, stdout, stderr, runner) = \ - self.run_booth(config_text=new_config, expected_exitcode=1) + self.run_booth(config_text=new_config, expected_exitcode=1, expected_daemon=False) self.assertRegexpMatches( self.read_log(), 'invalid config file format: unquoted whitespace' ) + + def test_unreachable_peer(self): + config = re.sub('#(.+147.+)', lambda m: m.group(1), self.working_config) + self.run_booth(config_text=config, + expected_exitcode=None, expected_daemon=False)