diff options
Diffstat (limited to 'bzrlib/tests/blackbox/test_serve.py')
-rw-r--r-- | bzrlib/tests/blackbox/test_serve.py | 450 |
1 files changed, 450 insertions, 0 deletions
diff --git a/bzrlib/tests/blackbox/test_serve.py b/bzrlib/tests/blackbox/test_serve.py new file mode 100644 index 0000000..c709e4e --- /dev/null +++ b/bzrlib/tests/blackbox/test_serve.py @@ -0,0 +1,450 @@ +# Copyright (C) 2006-2011 Canonical Ltd +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + +"""Tests of the bzr serve command.""" + +import os +import signal +import sys +import thread +import threading + +from bzrlib import ( + builtins, + config, + errors, + osutils, + revision as _mod_revision, + trace, + transport, + urlutils, + ) +from bzrlib.branch import Branch +from bzrlib.controldir import ControlDir +from bzrlib.smart import client, medium +from bzrlib.smart.server import ( + BzrServerFactory, + SmartTCPServer, + ) +from bzrlib.tests import ( + TestCaseWithMemoryTransport, + TestCaseWithTransport, + ) +from bzrlib.transport import remote + + +class TestBzrServeBase(TestCaseWithTransport): + + def run_bzr_serve_then_func(self, serve_args, retcode=0, func=None, + *func_args, **func_kwargs): + """Run 'bzr serve', and run the given func in a thread once the server + has started. + + When 'func' terminates, the server will be terminated too. + + Returns stdout and stderr. + """ + def on_server_start_thread(tcp_server): + """This runs concurrently with the server thread. + + The server is interrupted as soon as ``func`` finishes, even if an + exception is encountered. + """ + try: + # Run func if set + self.tcp_server = tcp_server + if func is not None: + try: + func(*func_args, **func_kwargs) + except Exception, e: + # Log errors to make some test failures a little less + # mysterious. + trace.mutter('func broke: %r', e) + finally: + # Then stop the server + trace.mutter('interrupting...') + thread.interrupt_main() + # When the hook is fired, it just starts ``on_server_start_thread`` and + # return + def on_server_start(backing_urls, tcp_server): + t = threading.Thread( + target=on_server_start_thread, args=(tcp_server,)) + t.start() + # install hook + SmartTCPServer.hooks.install_named_hook( + 'server_started_ex', on_server_start, + 'run_bzr_serve_then_func hook') + # It seesm thread.interrupt_main() will not raise KeyboardInterrupt + # until after socket.accept returns. So we set the timeout low to make + # the test faster. + self.overrideAttr(SmartTCPServer, '_ACCEPT_TIMEOUT', 0.1) + # start a TCP server + try: + out, err = self.run_bzr(['serve'] + list(serve_args), + retcode=retcode) + except KeyboardInterrupt, e: + out, err = e.args + return out, err + + +class TestBzrServe(TestBzrServeBase): + + def setUp(self): + super(TestBzrServe, self).setUp() + self.disable_missing_extensions_warning() + + def test_server_exception_with_hook(self): + """Catch exception from the server in the server_exception hook. + + We use ``run_bzr_serve_then_func`` without a ``func`` so the server + will receive a KeyboardInterrupt exception we want to catch. + """ + def hook(exception): + if exception[0] is KeyboardInterrupt: + sys.stderr.write('catching KeyboardInterrupt\n') + return True + else: + return False + SmartTCPServer.hooks.install_named_hook( + 'server_exception', hook, + 'test_server_except_hook hook') + args = ['--listen', 'localhost', '--port', '0', '--quiet'] + out, err = self.run_bzr_serve_then_func(args, retcode=0) + self.assertEqual('catching KeyboardInterrupt\n', err) + + def test_server_exception_no_hook(self): + """test exception without hook returns error""" + args = [] + out, err = self.run_bzr_serve_then_func(args, retcode=3) + + def assertInetServerShutsdownCleanly(self, process): + """Shutdown the server process looking for errors.""" + # Shutdown the server: the server should shut down when it cannot read + # from stdin anymore. + process.stdin.close() + # Hide stdin from the subprocess module, so it won't fail to close it. + process.stdin = None + result = self.finish_bzr_subprocess(process) + self.assertEqual('', result[0]) + self.assertEqual('', result[1]) + + def assertServerFinishesCleanly(self, process): + """Shutdown the bzr serve instance process looking for errors.""" + # Shutdown the server + result = self.finish_bzr_subprocess(process, retcode=3, + send_signal=signal.SIGINT) + self.assertEqual('', result[0]) + self.assertEqual('bzr: interrupted\n', result[1]) + + def make_read_requests(self, branch): + """Do some read only requests.""" + branch.lock_read() + try: + branch.repository.all_revision_ids() + self.assertEqual(_mod_revision.NULL_REVISION, + _mod_revision.ensure_null(branch.last_revision())) + finally: + branch.unlock() + + def start_server_inet(self, extra_options=()): + """Start a bzr server subprocess using the --inet option. + + :param extra_options: extra options to give the server. + :return: a tuple with the bzr process handle for passing to + finish_bzr_subprocess, a client for the server, and a transport. + """ + # Serve from the current directory + args = ['serve', '--inet'] + args.extend(extra_options) + process = self.start_bzr_subprocess(args) + + # Connect to the server + # We use this url because while this is no valid URL to connect to this + # server instance, the transport needs a URL. + url = 'bzr://localhost/' + self.permit_url(url) + client_medium = medium.SmartSimplePipesClientMedium( + process.stdout, process.stdin, url) + transport = remote.RemoteTransport(url, medium=client_medium) + return process, transport + + def start_server_port(self, extra_options=()): + """Start a bzr server subprocess. + + :param extra_options: extra options to give the server. + :return: a tuple with the bzr process handle for passing to + finish_bzr_subprocess, and the base url for the server. + """ + # Serve from the current directory + args = ['serve', '--listen', 'localhost', '--port', '0'] + args.extend(extra_options) + process = self.start_bzr_subprocess(args, skip_if_plan_to_signal=True) + port_line = process.stderr.readline() + prefix = 'listening on port: ' + self.assertStartsWith(port_line, prefix) + port = int(port_line[len(prefix):]) + url = 'bzr://localhost:%d/' % port + self.permit_url(url) + return process, url + + def test_bzr_serve_quiet(self): + self.make_branch('.') + args = ['--listen', 'localhost', '--port', '0', '--quiet'] + out, err = self.run_bzr_serve_then_func(args, retcode=3) + self.assertEqual('', out) + self.assertEqual('', err) + + def test_bzr_serve_inet_readonly(self): + """bzr server should provide a read only filesystem by default.""" + process, transport = self.start_server_inet() + self.assertRaises(errors.TransportNotPossible, transport.mkdir, 'adir') + self.assertInetServerShutsdownCleanly(process) + + def test_bzr_serve_inet_readwrite(self): + # Make a branch + self.make_branch('.') + + process, transport = self.start_server_inet(['--allow-writes']) + + # We get a working branch, and can create a directory + branch = ControlDir.open_from_transport(transport).open_branch() + self.make_read_requests(branch) + transport.mkdir('adir') + self.assertInetServerShutsdownCleanly(process) + + def test_bzr_serve_port_readonly(self): + """bzr server should provide a read only filesystem by default.""" + process, url = self.start_server_port() + t = transport.get_transport_from_url(url) + self.assertRaises(errors.TransportNotPossible, t.mkdir, 'adir') + self.assertServerFinishesCleanly(process) + + def test_bzr_serve_port_readwrite(self): + # Make a branch + self.make_branch('.') + + process, url = self.start_server_port(['--allow-writes']) + + # Connect to the server + branch = Branch.open(url) + self.make_read_requests(branch) + self.assertServerFinishesCleanly(process) + + def test_bzr_serve_supports_protocol(self): + # Make a branch + self.make_branch('.') + + process, url = self.start_server_port(['--allow-writes', + '--protocol=bzr']) + + # Connect to the server + branch = Branch.open(url) + self.make_read_requests(branch) + self.assertServerFinishesCleanly(process) + + def test_bzr_serve_dhpss(self): + # This is a smoke test that the server doesn't crash when run with + # -Dhpss, and does drop some hpss logging to the file. + self.make_branch('.') + log_fname = os.getcwd() + '/server.log' + self.overrideEnv('BZR_LOG', log_fname) + process, transport = self.start_server_inet(['-Dhpss']) + branch = ControlDir.open_from_transport(transport).open_branch() + self.make_read_requests(branch) + self.assertInetServerShutsdownCleanly(process) + f = open(log_fname, 'rb') + content = f.read() + f.close() + self.assertContainsRe(content, r'hpss request: \[[0-9-]+\]') + + def test_bzr_serve_supports_configurable_timeout(self): + gs = config.GlobalStack() + gs.set('serve.client_timeout', 0.2) + process, url = self.start_server_port() + self.build_tree_contents([('a_file', 'contents\n')]) + # We can connect and issue a request + t = transport.get_transport_from_url(url) + self.assertEqual('contents\n', t.get_bytes('a_file')) + # However, if we just wait for more content from the server, it will + # eventually disconnect us. + # TODO: Use something like signal.alarm() so that if the server doesn't + # properly handle the timeout, we end up failing the test instead + # of hanging forever. + m = t.get_smart_medium() + m.read_bytes(1) + # Now, we wait for timeout to trigger + err = process.stderr.readline() + self.assertEqual( + 'Connection Timeout: disconnecting client after 0.2 seconds\n', + err) + self.assertServerFinishesCleanly(process) + + def test_bzr_serve_supports_client_timeout(self): + process, url = self.start_server_port(['--client-timeout=0.1']) + self.build_tree_contents([('a_file', 'contents\n')]) + # We can connect and issue a request + t = transport.get_transport_from_url(url) + self.assertEqual('contents\n', t.get_bytes('a_file')) + # However, if we just wait for more content from the server, it will + # eventually disconnect us. + # TODO: Use something like signal.alarm() so that if the server doesn't + # properly handle the timeout, we end up failing the test instead + # of hanging forever. + m = t.get_smart_medium() + m.read_bytes(1) + # Now, we wait for timeout to trigger + err = process.stderr.readline() + self.assertEqual( + 'Connection Timeout: disconnecting client after 0.1 seconds\n', + err) + self.assertServerFinishesCleanly(process) + + def test_bzr_serve_graceful_shutdown(self): + big_contents = 'a'*64*1024 + self.build_tree_contents([('bigfile', big_contents)]) + process, url = self.start_server_port(['--client-timeout=1.0']) + t = transport.get_transport_from_url(url) + m = t.get_smart_medium() + c = client._SmartClient(m) + # Start, but don't finish a response + resp, response_handler = c.call_expecting_body('get', 'bigfile') + self.assertEqual(('ok',), resp) + # Note: process.send_signal is a Python 2.6ism + process.send_signal(signal.SIGHUP) + # Wait for the server to notice the signal, and then read the actual + # body of the response. That way we know that it is waiting for the + # request to finish + self.assertEqual('Requested to stop gracefully\n', + process.stderr.readline()) + self.assertEqual('Waiting for 1 client(s) to finish\n', + process.stderr.readline()) + body = response_handler.read_body_bytes() + if body != big_contents: + self.fail('Failed to properly read the contents of "bigfile"') + # Now that our request is finished, the medium should notice it has + # been disconnected. + self.assertEqual('', m.read_bytes(1)) + # And the server should be stopping + self.assertEqual(0, process.wait()) + + +class TestCmdServeChrooting(TestBzrServeBase): + + def test_serve_tcp(self): + """'bzr serve' wraps the given --directory in a ChrootServer. + + So requests that search up through the parent directories (like + find_repositoryV3) will give "not found" responses, rather than + InvalidURLJoin or jail break errors. + """ + t = self.get_transport() + t.mkdir('server-root') + self.run_bzr_serve_then_func( + ['--listen', '127.0.0.1', '--port', '0', + '--directory', t.local_abspath('server-root'), + '--allow-writes'], + func=self.when_server_started) + # The when_server_started method issued a find_repositoryV3 that should + # fail with 'norepository' because there are no repositories inside the + # --directory. + self.assertEqual(('norepository',), self.client_resp) + + def when_server_started(self): + # Connect to the TCP server and issue some requests and see what comes + # back. + client_medium = medium.SmartTCPClientMedium( + '127.0.0.1', self.tcp_server.port, + 'bzr://localhost:%d/' % (self.tcp_server.port,)) + smart_client = client._SmartClient(client_medium) + resp = smart_client.call('mkdir', 'foo', '') + resp = smart_client.call('BzrDirFormat.initialize', 'foo/') + try: + resp = smart_client.call('BzrDir.find_repositoryV3', 'foo/') + except errors.ErrorFromSmartServer, e: + resp = e.error_tuple + self.client_resp = resp + client_medium.disconnect() + + +class TestUserdirExpansion(TestCaseWithMemoryTransport): + + @staticmethod + def fake_expanduser(path): + """A simple, environment-independent, function for the duration of this + test. + + Paths starting with a path segment of '~user' will expand to start with + '/home/user/'. Every other path will be unchanged. + """ + if path.split('/', 1)[0] == '~user': + return '/home/user' + path[len('~user'):] + return path + + def make_test_server(self, base_path='/'): + """Make and start a BzrServerFactory, backed by a memory transport, and + creat '/home/user' in that transport. + """ + bzr_server = BzrServerFactory( + self.fake_expanduser, lambda t: base_path) + mem_transport = self.get_transport() + mem_transport.mkdir_multi(['home', 'home/user']) + bzr_server.set_up(mem_transport, None, None, inet=True, timeout=4.0) + self.addCleanup(bzr_server.tear_down) + return bzr_server + + def test_bzr_serve_expands_userdir(self): + bzr_server = self.make_test_server() + self.assertTrue(bzr_server.smart_server.backing_transport.has('~user')) + + def test_bzr_serve_does_not_expand_userdir_outside_base(self): + bzr_server = self.make_test_server('/foo') + self.assertFalse(bzr_server.smart_server.backing_transport.has('~user')) + + def test_get_base_path(self): + """cmd_serve will turn the --directory option into a LocalTransport + (optionally decorated with 'readonly+'). BzrServerFactory can + determine the original --directory from that transport. + """ + # URLs always include the trailing slash, and get_base_path returns it + base_dir = osutils.abspath('/a/b/c') + '/' + base_url = urlutils.local_path_to_url(base_dir) + '/' + # Define a fake 'protocol' to capture the transport that cmd_serve + # passes to serve_bzr. + def capture_transport(transport, host, port, inet, timeout): + self.bzr_serve_transport = transport + cmd = builtins.cmd_serve() + # Read-only + cmd.run(directory=base_dir, protocol=capture_transport) + server_maker = BzrServerFactory() + self.assertEqual( + 'readonly+%s' % base_url, self.bzr_serve_transport.base) + self.assertEqual( + base_dir, server_maker.get_base_path(self.bzr_serve_transport)) + # Read-write + cmd.run(directory=base_dir, protocol=capture_transport, + allow_writes=True) + server_maker = BzrServerFactory() + self.assertEqual(base_url, self.bzr_serve_transport.base) + self.assertEqual(base_dir, + server_maker.get_base_path(self.bzr_serve_transport)) + # Read-only, from a URL + cmd.run(directory=base_url, protocol=capture_transport) + server_maker = BzrServerFactory() + self.assertEqual( + 'readonly+%s' % base_url, self.bzr_serve_transport.base) + self.assertEqual( + base_dir, server_maker.get_base_path(self.bzr_serve_transport)) |