class LaunchpadTransportTests: """Tests for a Launchpad transport. These tests are expected to run against two kinds of transport. 1. An asynchronous one that returns Deferreds. 2. A synchronous one that returns actual values. To support that, subclasses must implement `getTransport` and `_ensureDeferred`. See these methods for more information. """ def setUp(self): frontend = InMemoryFrontend() self.factory = frontend.getLaunchpadObjectFactory() codehosting_api = frontend.getCodehostingEndpoint() self.requester = self.factory.makePerson() self.backing_transport = MemoryTransport() self.server = self.getServer(codehosting_api, self.requester.id, self.backing_transport) self.server.start_server() self.addCleanup(self.server.stop_server) def assertFiresFailure(self, exception, function, *args, **kwargs): """Assert that calling `function` will cause `exception` to be fired. In the synchronous tests, this means that `function` raises `exception`. In the asynchronous tests, `function` returns a Deferred that fires `exception` as a Failure. :return: A `Deferred`. You must return this from your test. """ return assert_fails_with( self._ensureDeferred(function, *args, **kwargs), exception) def assertFiresFailureWithSubstring(self, exc_type, msg, function, *args, **kw): """Assert that calling function(*args, **kw) fails in a certain way. This method is like assertFiresFailure() but in addition checks that 'msg' is a substring of the str() of the raised exception. """ deferred = self.assertFiresFailure(exc_type, function, *args, **kw) return deferred.addCallback( lambda exception: self.assertIn(msg, str(exception))) def _ensureDeferred(self, function, *args, **kwargs): """Call `function` and return an appropriate Deferred.""" raise NotImplementedError def getServer(self, codehosting_api, user_id, backing_transport): return LaunchpadServer(XMLRPCWrapper(codehosting_api), user_id, backing_transport) def getTransport(self): """Return the transport to be tested.""" raise NotImplementedError() def test_get_transport(self): # When the server is set up, getting a transport for the server URL # returns a LaunchpadTransport pointing at that URL. That is, the # transport is registered once the server is set up. transport = self.getTransport() self.assertEqual(self.server.get_url(), transport.base) def test_cant_write_to_control_conf(self): # You can't write to the control.conf file if it exists. It's # generated by Launchpad based on info in the database, rather than # being an actual file on disk. transport = self.getTransport() branch = self.factory.makeProductBranch(branch_type=BranchType.HOSTED, owner=self.requester) self.factory.enableDefaultStackingForProduct(branch.product, branch) return self.assertFiresFailure( errors.TransportNotPossible, transport.put_bytes, '~%s/%s/.bzr/control.conf' % (branch.owner.name, branch.product.name), 'hello nurse!') def _makeOnBackingTransport(self, branch): """Make directories for 'branch' on the backing transport. :return: a transport for the .bzr directory of 'branch'. """ backing_transport = self.backing_transport.clone( '%s/.bzr/' % branch_to_path(branch, add_slash=False)) backing_transport.create_prefix() return backing_transport def test_get_mapped_file(self): # Getting a file from a public branch URL gets the file as stored on # the base transport. transport = self.getTransport() branch = self.factory.makeAnyBranch(branch_type=BranchType.HOSTED, owner=self.requester) backing_transport = self._makeOnBackingTransport(branch) backing_transport.put_bytes('hello.txt', 'Hello World!') deferred = self._ensureDeferred( transport.get_bytes, '%s/.bzr/hello.txt' % branch.unique_name) return deferred.addCallback(self.assertEqual, 'Hello World!') def test_get_mapped_file_escaped_url(self): # Getting a file from a public branch URL gets the file as stored on # the base transport, even when the URL is escaped. branch = self.factory.makeAnyBranch(branch_type=BranchType.HOSTED, owner=self.requester) backing_transport = self._makeOnBackingTransport(branch) backing_transport.put_bytes('hello.txt', 'Hello World!') url = escape('%s/.bzr/hello.txt' % branch.unique_name) transport = self.getTransport() deferred = self._ensureDeferred(transport.get_bytes, url) return deferred.addCallback(self.assertEqual, 'Hello World!') def test_readv_mapped_file(self): # Using readv on a public branch URL gets chunks of the file as stored # on the base transport. branch = self.factory.makeAnyBranch(branch_type=BranchType.HOSTED, owner=self.requester) backing_transport = self._makeOnBackingTransport(branch) data = 'Hello World!' backing_transport.put_bytes('hello.txt', data) transport = self.getTransport() deferred = self._ensureDeferred( transport.readv, '%s/.bzr/hello.txt' % branch.unique_name, [(3, 2)]) def get_chunk(generator): return generator.next()[1] deferred.addCallback(get_chunk) return deferred.addCallback(self.assertEqual, data[3:5]) def test_put_mapped_file(self): # Putting a file from a public branch URL stores the file in the # mapped URL on the base transport. transport = self.getTransport() branch = self.factory.makeAnyBranch(branch_type=BranchType.HOSTED, owner=self.requester) backing_transport = self._makeOnBackingTransport(branch) deferred = self._ensureDeferred( transport.put_bytes, '%s/.bzr/goodbye.txt' % branch.unique_name, "Goodbye") def check_bytes_written(ignored): self.assertEqual("Goodbye", backing_transport.get_bytes('goodbye.txt')) return deferred.addCallback(check_bytes_written) def test_cloning_updates_base(self): # A transport can be constructed using a path relative to another # transport by using 'clone'. When this happens, it's necessary for # the newly constructed transport to preserve the non-relative path # information from the transport being cloned. It's necessary because # the transport needs to have the '~user/product/branch-name' in order # to translate paths. transport = self.getTransport() self.assertEqual(self.server.get_url(), transport.base) transport = transport.clone('~testuser') self.assertEqual(self.server.get_url() + '~testuser', transport.base) def test_abspath_without_schema(self): # _abspath returns the absolute path for a given relative path, but # without the schema part of the URL that is included by abspath. transport = self.getTransport() self.assertEqual('/~testuser/firefox/baz', transport._abspath('~testuser/firefox/baz')) transport = transport.clone('~testuser') self.assertEqual('/~testuser/firefox/baz', transport._abspath('firefox/baz')) def test_cloning_preserves_path_mapping(self): # The public branch URL -> filesystem mapping uses the base URL to do # its mapping, thus ensuring that clones map correctly. transport = self.getTransport() branch = self.factory.makeAnyBranch(branch_type=BranchType.HOSTED, owner=self.requester) backing_transport = self._makeOnBackingTransport(branch) backing_transport.put_bytes('hello.txt', 'Hello World!') transport = transport.clone('~%s' % branch.owner.name) deferred = self._ensureDeferred( transport.get_bytes, '%s/%s/.bzr/hello.txt' % (branch.product.name, branch.name)) return deferred.addCallback(self.assertEqual, 'Hello World!') def test_abspath(self): # abspath for a relative path is the same as the base URL for a clone # for that relative path. transport = self.getTransport() self.assertEqual( transport.clone('~testuser').base, transport.abspath('~testuser')) def test_incomplete_path_not_found(self): # For a branch URL to be complete, it needs to have a person, product # and branch. Trying to perform operations on an incomplete URL raises # an error. Which kind of error is not particularly important. transport = self.getTransport() return self.assertFiresFailure(errors.NoSuchFile, transport.get, '~testuser') def test_complete_non_existent_path_not_found(self): # Bazaar looks for files inside a branch directory before it looks for # the branch itself. If the branch doesn't exist, any files it asks # for are not found. i.e. we raise NoSuchFile transport = self.getTransport() return self.assertFiresFailure( errors.NoSuchFile, transport.get, '~testuser/firefox/new-branch/.bzr/branch-format') def test_rename(self): # We can use the transport to rename files where both the source and # target are virtual paths. branch = self.factory.makeAnyBranch(branch_type=BranchType.HOSTED, owner=self.requester) backing_transport = self._makeOnBackingTransport(branch) backing_transport.put_bytes('hello.txt', 'Hello World!') transport = self.getTransport().clone(branch.unique_name) deferred = self._ensureDeferred(transport.list_dir, '.bzr') deferred.addCallback(set) def rename_file(dir_contents): """Rename a file and return the original directory contents.""" deferred = self._ensureDeferred(transport.rename, '.bzr/hello.txt', '.bzr/goodbye.txt') deferred.addCallback(lambda ignored: dir_contents) return deferred def check_file_was_renamed(dir_contents): """Check that the old name isn't there and the new name is.""" # Replace the old name with the new name. dir_contents.remove('hello.txt') dir_contents.add('goodbye.txt') deferred = self._ensureDeferred(transport.list_dir, '.bzr') deferred.addCallback(set) # Check against the virtual transport. deferred.addCallback(self.assertEqual, dir_contents) # Check against the backing transport. deferred.addCallback(lambda ignored: self.assertEqual( set(backing_transport.list_dir('.')), dir_contents)) return deferred deferred.addCallback(rename_file) return deferred.addCallback(check_file_was_renamed) def test_iter_files_recursive(self): # iter_files_recursive doesn't take a relative path but still needs to # do a path-based operation on the backing transport, so the # implementation can't just be a shim to the backing transport. branch = self.factory.makeAnyBranch(branch_type=BranchType.HOSTED, owner=self.requester) backing_transport = self._makeOnBackingTransport(branch) backing_transport.put_bytes('hello.txt', 'Hello World!') transport = self.getTransport().clone(branch.unique_name) backing_transport = self.backing_transport.clone( branch_to_path(branch)) deferred = self._ensureDeferred(transport.iter_files_recursive) def check_iter_result(iter_files, expected_files): self.assertEqual(expected_files, list(iter_files)) deferred.addCallback(check_iter_result, list(backing_transport.iter_files_recursive())) return deferred def test_make_two_directories(self): # Bazaar doesn't have a makedirs() facility for transports, so we need # to make sure that we can make a directory on the backing transport # if its parents exist and if they don't exist. product = self.factory.makeProduct() banana = '~%s/%s/banana' % (self.requester.name, product.name) orange = '~%s/%s/orange' % (self.requester.name, product.name) transport = self.getTransport() transport.mkdir(banana) transport.mkdir(orange) self.assertTrue(transport.has(banana)) self.assertTrue(transport.has(orange)) def test_createBranch_not_found_error(self): # When createBranch raises faults.NotFound the transport should # translate this to a PermissionDenied exception (see the comment in # transport.py for why we translate to TransportNotPossible and not # NoSuchFile). transport = self.getTransport() return self.assertFiresFailureWithSubstring( errors.PermissionDenied, "does not exist", transport.mkdir, '~%s/no-such-product/some-name' % self.requester.name) def test_createBranch_permission_denied_error(self): # When createBranch raises faults.PermissionDenied, the transport # should translate this to a PermissionDenied exception. transport = self.getTransport() person = self.factory.makePerson() product = self.factory.makeProduct() message = ("%s cannot create branches owned by %s" % (self.requester.displayname, person.displayname)) return self.assertFiresFailureWithSubstring( errors.PermissionDenied, message, transport.mkdir, '~%s/%s/some-name' % (person.name, product.name)) def test_createBranch_invalid_package_name(self): # When createBranch raises faults.InvalidSourcePackageName, the # transport should translate this to a PermissionDenied exception transport = self.getTransport() series = self.factory.makeDistroSeries() unique_name = '~%s/%s/%s/spaced%%20name/branch' % ( self.requester.name, series.distribution.name, series.name) return self.assertFiresFailureWithSubstring( errors.PermissionDenied, "is not a valid source package name", transport.mkdir, unique_name) def test_rmdir(self): transport = self.getTransport() self.assertFiresFailure(errors.PermissionDenied, transport.rmdir, '~testuser/firefox/baz')
def test_clone(self): transport = MemoryTransport() self.assertTrue(isinstance(transport, MemoryTransport)) self.assertEqual("memory:///", transport.clone("/").base)
def test_clone(self): transport = MemoryTransport() self.assertTrue(isinstance(transport, MemoryTransport)) self.assertEqual("memory:///", transport.clone("/").base)
class LaunchpadTransportTests: """Tests for a Launchpad transport. These tests are expected to run against two kinds of transport. 1. An asynchronous one that returns Deferreds. 2. A synchronous one that returns actual values. To support that, subclasses must implement `getTransport` and `_ensureDeferred`. See these methods for more information. """ def setUp(self): frontend = InMemoryFrontend() self.factory = frontend.getLaunchpadObjectFactory() codehosting_api = frontend.getCodehostingEndpoint() self.requester = self.factory.makePerson() self.backing_transport = MemoryTransport() self.server = self.getServer( codehosting_api, self.requester.id, self.backing_transport) self.server.start_server() self.addCleanup(self.server.stop_server) def assertFiresFailure(self, exception, function, *args, **kwargs): """Assert that calling `function` will cause `exception` to be fired. In the synchronous tests, this means that `function` raises `exception`. In the asynchronous tests, `function` returns a Deferred that fires `exception` as a Failure. :return: A `Deferred`. You must return this from your test. """ return assert_fails_with( self._ensureDeferred(function, *args, **kwargs), exception) def assertFiresFailureWithSubstring(self, exc_type, msg, function, *args, **kw): """Assert that calling function(*args, **kw) fails in a certain way. This method is like assertFiresFailure() but in addition checks that 'msg' is a substring of the str() of the raised exception. """ deferred = self.assertFiresFailure(exc_type, function, *args, **kw) return deferred.addCallback( lambda exception: self.assertIn(msg, str(exception))) def _ensureDeferred(self, function, *args, **kwargs): """Call `function` and return an appropriate Deferred.""" raise NotImplementedError def getServer(self, codehosting_api, user_id, backing_transport): return LaunchpadServer( XMLRPCWrapper(codehosting_api), user_id, backing_transport) def getTransport(self): """Return the transport to be tested.""" raise NotImplementedError() def test_get_transport(self): # When the server is set up, getting a transport for the server URL # returns a LaunchpadTransport pointing at that URL. That is, the # transport is registered once the server is set up. transport = self.getTransport() self.assertEqual(self.server.get_url(), transport.base) def test_cant_write_to_control_conf(self): # You can't write to the control.conf file if it exists. It's # generated by Launchpad based on info in the database, rather than # being an actual file on disk. transport = self.getTransport() branch = self.factory.makeProductBranch( branch_type=BranchType.HOSTED, owner=self.requester) self.factory.enableDefaultStackingForProduct(branch.product, branch) return self.assertFiresFailure( errors.TransportNotPossible, transport.put_bytes, '~%s/%s/.bzr/control.conf' % ( branch.owner.name, branch.product.name), 'hello nurse!') def _makeOnBackingTransport(self, branch): """Make directories for 'branch' on the backing transport. :return: a transport for the .bzr directory of 'branch'. """ backing_transport = self.backing_transport.clone( '%s/.bzr/' % branch_to_path(branch, add_slash=False)) backing_transport.create_prefix() return backing_transport def test_get_mapped_file(self): # Getting a file from a public branch URL gets the file as stored on # the base transport. transport = self.getTransport() branch = self.factory.makeAnyBranch( branch_type=BranchType.HOSTED, owner=self.requester) backing_transport = self._makeOnBackingTransport(branch) backing_transport.put_bytes('hello.txt', 'Hello World!') deferred = self._ensureDeferred( transport.get_bytes, '%s/.bzr/hello.txt' % branch.unique_name) return deferred.addCallback(self.assertEqual, 'Hello World!') def test_get_mapped_file_escaped_url(self): # Getting a file from a public branch URL gets the file as stored on # the base transport, even when the URL is escaped. branch = self.factory.makeAnyBranch( branch_type=BranchType.HOSTED, owner=self.requester) backing_transport = self._makeOnBackingTransport(branch) backing_transport.put_bytes('hello.txt', 'Hello World!') url = escape('%s/.bzr/hello.txt' % branch.unique_name) transport = self.getTransport() deferred = self._ensureDeferred(transport.get_bytes, url) return deferred.addCallback(self.assertEqual, 'Hello World!') def test_readv_mapped_file(self): # Using readv on a public branch URL gets chunks of the file as stored # on the base transport. branch = self.factory.makeAnyBranch( branch_type=BranchType.HOSTED, owner=self.requester) backing_transport = self._makeOnBackingTransport(branch) data = 'Hello World!' backing_transport.put_bytes('hello.txt', data) transport = self.getTransport() deferred = self._ensureDeferred( transport.readv, '%s/.bzr/hello.txt' % branch.unique_name, [(3, 2)]) def get_chunk(generator): return generator.next()[1] deferred.addCallback(get_chunk) return deferred.addCallback(self.assertEqual, data[3:5]) def test_put_mapped_file(self): # Putting a file from a public branch URL stores the file in the # mapped URL on the base transport. transport = self.getTransport() branch = self.factory.makeAnyBranch( branch_type=BranchType.HOSTED, owner=self.requester) backing_transport = self._makeOnBackingTransport(branch) deferred = self._ensureDeferred( transport.put_bytes, '%s/.bzr/goodbye.txt' % branch.unique_name, "Goodbye") def check_bytes_written(ignored): self.assertEqual( "Goodbye", backing_transport.get_bytes('goodbye.txt')) return deferred.addCallback(check_bytes_written) def test_cloning_updates_base(self): # A transport can be constructed using a path relative to another # transport by using 'clone'. When this happens, it's necessary for # the newly constructed transport to preserve the non-relative path # information from the transport being cloned. It's necessary because # the transport needs to have the '~user/product/branch-name' in order # to translate paths. transport = self.getTransport() self.assertEqual(self.server.get_url(), transport.base) transport = transport.clone('~testuser') self.assertEqual(self.server.get_url() + '~testuser', transport.base) def test_abspath_without_schema(self): # _abspath returns the absolute path for a given relative path, but # without the schema part of the URL that is included by abspath. transport = self.getTransport() self.assertEqual( '/~testuser/firefox/baz', transport._abspath('~testuser/firefox/baz')) transport = transport.clone('~testuser') self.assertEqual( '/~testuser/firefox/baz', transport._abspath('firefox/baz')) def test_cloning_preserves_path_mapping(self): # The public branch URL -> filesystem mapping uses the base URL to do # its mapping, thus ensuring that clones map correctly. transport = self.getTransport() branch = self.factory.makeAnyBranch( branch_type=BranchType.HOSTED, owner=self.requester) backing_transport = self._makeOnBackingTransport(branch) backing_transport.put_bytes('hello.txt', 'Hello World!') transport = transport.clone('~%s' % branch.owner.name) deferred = self._ensureDeferred( transport.get_bytes, '%s/%s/.bzr/hello.txt' % (branch.product.name, branch.name)) return deferred.addCallback(self.assertEqual, 'Hello World!') def test_abspath(self): # abspath for a relative path is the same as the base URL for a clone # for that relative path. transport = self.getTransport() self.assertEqual( transport.clone('~testuser').base, transport.abspath('~testuser')) def test_incomplete_path_not_found(self): # For a branch URL to be complete, it needs to have a person, product # and branch. Trying to perform operations on an incomplete URL raises # an error. Which kind of error is not particularly important. transport = self.getTransport() return self.assertFiresFailure( errors.NoSuchFile, transport.get, '~testuser') def test_complete_non_existent_path_not_found(self): # Bazaar looks for files inside a branch directory before it looks for # the branch itself. If the branch doesn't exist, any files it asks # for are not found. i.e. we raise NoSuchFile transport = self.getTransport() return self.assertFiresFailure( errors.NoSuchFile, transport.get, '~testuser/firefox/new-branch/.bzr/branch-format') def test_rename(self): # We can use the transport to rename files where both the source and # target are virtual paths. branch = self.factory.makeAnyBranch( branch_type=BranchType.HOSTED, owner=self.requester) backing_transport = self._makeOnBackingTransport(branch) backing_transport.put_bytes('hello.txt', 'Hello World!') transport = self.getTransport().clone(branch.unique_name) deferred = self._ensureDeferred(transport.list_dir, '.bzr') deferred.addCallback(set) def rename_file(dir_contents): """Rename a file and return the original directory contents.""" deferred = self._ensureDeferred( transport.rename, '.bzr/hello.txt', '.bzr/goodbye.txt') deferred.addCallback(lambda ignored: dir_contents) return deferred def check_file_was_renamed(dir_contents): """Check that the old name isn't there and the new name is.""" # Replace the old name with the new name. dir_contents.remove('hello.txt') dir_contents.add('goodbye.txt') deferred = self._ensureDeferred(transport.list_dir, '.bzr') deferred.addCallback(set) # Check against the virtual transport. deferred.addCallback(self.assertEqual, dir_contents) # Check against the backing transport. deferred.addCallback( lambda ignored: self.assertEqual( set(backing_transport.list_dir('.')), dir_contents)) return deferred deferred.addCallback(rename_file) return deferred.addCallback(check_file_was_renamed) def test_iter_files_recursive(self): # iter_files_recursive doesn't take a relative path but still needs to # do a path-based operation on the backing transport, so the # implementation can't just be a shim to the backing transport. branch = self.factory.makeAnyBranch( branch_type=BranchType.HOSTED, owner=self.requester) backing_transport = self._makeOnBackingTransport(branch) backing_transport.put_bytes('hello.txt', 'Hello World!') transport = self.getTransport().clone(branch.unique_name) backing_transport = self.backing_transport.clone( branch_to_path(branch)) deferred = self._ensureDeferred(transport.iter_files_recursive) def check_iter_result(iter_files, expected_files): self.assertEqual(expected_files, list(iter_files)) deferred.addCallback( check_iter_result, list(backing_transport.iter_files_recursive())) return deferred def test_make_two_directories(self): # Bazaar doesn't have a makedirs() facility for transports, so we need # to make sure that we can make a directory on the backing transport # if its parents exist and if they don't exist. product = self.factory.makeProduct() banana = '~%s/%s/banana' % (self.requester.name, product.name) orange = '~%s/%s/orange' % (self.requester.name, product.name) transport = self.getTransport() transport.mkdir(banana) transport.mkdir(orange) self.assertTrue(transport.has(banana)) self.assertTrue(transport.has(orange)) def test_createBranch_not_found_error(self): # When createBranch raises faults.NotFound the transport should # translate this to a PermissionDenied exception (see the comment in # transport.py for why we translate to TransportNotPossible and not # NoSuchFile). transport = self.getTransport() return self.assertFiresFailureWithSubstring( errors.PermissionDenied, "does not exist", transport.mkdir, '~%s/no-such-product/some-name' % self.requester.name) def test_createBranch_permission_denied_error(self): # When createBranch raises faults.PermissionDenied, the transport # should translate this to a PermissionDenied exception. transport = self.getTransport() person = self.factory.makePerson() product = self.factory.makeProduct() message = ( "%s cannot create branches owned by %s" % (self.requester.displayname, person.displayname)) return self.assertFiresFailureWithSubstring( errors.PermissionDenied, message, transport.mkdir, '~%s/%s/some-name' % (person.name, product.name)) def test_createBranch_invalid_package_name(self): # When createBranch raises faults.InvalidSourcePackageName, the # transport should translate this to a PermissionDenied exception transport = self.getTransport() series = self.factory.makeDistroSeries() unique_name = '~%s/%s/%s/spaced%%20name/branch' % ( self.requester.name, series.distribution.name, series.name) return self.assertFiresFailureWithSubstring( errors.PermissionDenied, "is not a valid source package name", transport.mkdir, unique_name) def test_rmdir(self): transport = self.getTransport() self.assertFiresFailure( errors.PermissionDenied, transport.rmdir, '~testuser/firefox/baz')