Merge lp:~thumper/launchpad/xmlrpc-tests into lp:launchpad/db-devel

Proposed by Tim Penhey
Status: Merged
Merged at revision: 9758
Proposed branch: lp:~thumper/launchpad/xmlrpc-tests
Merge into: lp:launchpad/db-devel
Diff against target: 366 lines (+79/-145)
3 files modified
Makefile (+2/-2)
lib/lp/code/xmlrpc/branch.py (+45/-15)
lib/lp/code/xmlrpc/tests/test_branch.py (+32/-128)
To merge this branch: bzr merge lp:~thumper/launchpad/xmlrpc-tests
Reviewer Review Type Date Requested Status
Graham Binns (community) release-critical Approve
Launchpad code reviewers Pending
Review via email: mp+34719@code.launchpad.net

Commit message

Correctly resolve lp path names to use the +branch alias.

Description of the change

This branch addresses issues found when QA testing the new lp:short-name for private branches.

When going through the XMLRPC resolve_lp_path tests, I seem to have forgotten the complete point of the change, which was to not look up the branch in most cases.

In order to return the http urls for branches accessed using lp:foo a branch lookup is still needed until the branch rewrite map can understand the aliases.

Many tests were removed as they are now pointless as the checking for the linked branch has been passed on to the code path that resolves the aliased name (an earlier branch).

tests:
  TestExpandURL

To post a comment you must log in.
Revision history for this message
Tim Penhey (thumper) wrote :

The Makefile change was a drive-by that I needed in order to run some interactive testing. The hosted_branches make target calls a broken script right now, and that was out of scope for this change.

Revision history for this message
Graham Binns (gmb) :
review: Approve (release-critical)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'Makefile'
--- Makefile 2010-09-05 20:29:40 +0000
+++ Makefile 2010-09-07 05:51:45 +0000
@@ -234,7 +234,7 @@
234 -r librarian,google-webservice234 -r librarian,google-webservice
235 > ${LPCONFIG}-nohup.out 2>&1 &235 > ${LPCONFIG}-nohup.out 2>&1 &
236236
237run_all: check_schema inplace stop hosted_branches237run_all: check_schema inplace stop
238 $(RM) thread*.request238 $(RM) thread*.request
239 bin/run -r librarian,sftp,mailman,codebrowse,google-webservice,memcached \239 bin/run -r librarian,sftp,mailman,codebrowse,google-webservice,memcached \
240 -i $(LPCONFIG)240 -i $(LPCONFIG)
@@ -248,7 +248,7 @@
248stop_codebrowse:248stop_codebrowse:
249 $(PY) scripts/stop-loggerhead.py249 $(PY) scripts/stop-loggerhead.py
250250
251run_codehosting: check_schema inplace stop hosted_branches251run_codehosting: check_schema inplace stop
252 $(RM) thread*.request252 $(RM) thread*.request
253 bin/run -r librarian,sftp,codebrowse -i $(LPCONFIG)253 bin/run -r librarian,sftp,codebrowse -i $(LPCONFIG)
254254
255255
=== modified file 'lib/lp/code/xmlrpc/branch.py'
--- lib/lp/code/xmlrpc/branch.py 2010-09-02 21:56:09 +0000
+++ lib/lp/code/xmlrpc/branch.py 2010-09-07 05:51:45 +0000
@@ -15,6 +15,8 @@
15 ]15 ]
1616
1717
18from xmlrpclib import Fault
19
18from bzrlib import urlutils20from bzrlib import urlutils
19from zope.component import getUtility21from zope.component import getUtility
20from zope.interface import (22from zope.interface import (
@@ -214,9 +216,9 @@
214 'http': _compose_http_url,216 'http': _compose_http_url,
215 }217 }
216218
217 def _getResultDict(self, branch, lp_path, suffix=None,219 def _getUrlsForBranch(self, branch, lp_path, suffix=None,
218 supported_schemes=None):220 supported_schemes=None):
219 """Return a result dict with a list of URLs for the given branch.221 """Return a list of URLs for the given branch.
220222
221 :param branch: A Branch object.223 :param branch: A Branch object.
222 :param lp_path: The path that was used to traverse to the branch.224 :param lp_path: The path that was used to traverse to the branch.
@@ -226,8 +228,8 @@
226 """228 """
227 if branch.branch_type == BranchType.REMOTE:229 if branch.branch_type == BranchType.REMOTE:
228 if branch.url is None:230 if branch.url is None:
229 return faults.NoUrlForBranch(branch.unique_name)231 raise faults.NoUrlForBranch(branch.unique_name)
230 return dict(urls=[branch.url])232 return [branch.url]
231 else:233 else:
232 return self._getUniqueNameResultDict(234 return self._getUniqueNameResultDict(
233 branch.unique_name, suffix, supported_schemes, lp_path)235 branch.unique_name, suffix, supported_schemes, lp_path)
@@ -238,11 +240,8 @@
238 supported_schemes = SUPPORTED_SCHEMES240 supported_schemes = SUPPORTED_SCHEMES
239 if path is None:241 if path is None:
240 path = unique_name242 path = unique_name
241 result = dict(urls=[])243 return [self.scheme_funcs[scheme](unique_name, path, suffix)
242 for scheme in supported_schemes:244 for scheme in supported_schemes]
243 result['urls'].append(
244 self.scheme_funcs[scheme](unique_name, path, suffix))
245 return result
246245
247 @return_fault246 @return_fault
248 def _resolve_lp_path(self, path):247 def _resolve_lp_path(self, path):
@@ -254,11 +253,39 @@
254 strip_path = path.strip('/')253 strip_path = path.strip('/')
255 if strip_path == '':254 if strip_path == '':
256 raise faults.InvalidBranchIdentifier(path)255 raise faults.InvalidBranchIdentifier(path)
257 supported_schemes = SUPPORTED_SCHEMES256 supported_schemes = list(SUPPORTED_SCHEMES)
258 hot_products = [product.strip() for product257 hot_products = [product.strip() for product
259 in config.codehosting.hot_products.split(',')]258 in config.codehosting.hot_products.split(',')]
260 if strip_path in hot_products:259 # If we have been given something that looks like a branch name, just
261 supported_schemes = ['http']260 # look that up.
261 if strip_path.startswith('~'):
262 urls = self._getBranchPaths(strip_path, supported_schemes)
263 else:
264 # We only check the hot product code when accessed through the
265 # short name, so we can check it here.
266 if strip_path in hot_products:
267 supported_schemes = ['http']
268 urls = []
269 else:
270 urls = [self.scheme_funcs['bzr+ssh'](None, strip_path, None)]
271 supported_schemes.remove('bzr+ssh')
272 # Try to look up the branch at that url and add alternative URLs.
273 # This may well fail, and if it does, we just return the aliased
274 # url.
275 try:
276 urls.extend(
277 self._getBranchPaths(strip_path, supported_schemes))
278 except Fault:
279 pass
280 return dict(urls=urls)
281
282 def _getBranchPaths(self, strip_path, supported_schemes):
283 """Get the specific paths for a branch.
284
285 If the branch is not found, but it looks like a branch name, then we
286 return a writable URL for it. If it doesn't look like a branch name a
287 fault is raised.
288 """
262 branch_set = getUtility(IBranchLookup)289 branch_set = getUtility(IBranchLookup)
263 try:290 try:
264 branch, suffix = branch_set.getByLPPath(strip_path)291 branch, suffix = branch_set.getByLPPath(strip_path)
@@ -267,7 +294,9 @@
267 # resolve it anyway, treating the path like a branch's unique294 # resolve it anyway, treating the path like a branch's unique
268 # name. This lets people push new branches up to Launchpad using295 # name. This lets people push new branches up to Launchpad using
269 # lp: URL syntax.296 # lp: URL syntax.
270 return self._getUniqueNameResultDict(strip_path)297 supported_schemes = ['bzr+ssh']
298 return self._getUniqueNameResultDict(
299 strip_path, supported_schemes=supported_schemes)
271 # XXX: JonathanLange 2009-03-21 bug=347728: All of this is repetitive300 # XXX: JonathanLange 2009-03-21 bug=347728: All of this is repetitive
272 # and thus error prone. Alternatives are directly raising faults from301 # and thus error prone. Alternatives are directly raising faults from
273 # the model code(blech) or some automated way of reraising as faults302 # the model code(blech) or some automated way of reraising as faults
@@ -298,7 +327,8 @@
298 # E.g. 'project/trunk/filename.txt' the suffix is 'filename.txt'327 # E.g. 'project/trunk/filename.txt' the suffix is 'filename.txt'
299 # we want lp_path to be 'project/trunk'.328 # we want lp_path to be 'project/trunk'.
300 lp_path = lp_path[:-(len(suffix)+1)]329 lp_path = lp_path[:-(len(suffix)+1)]
301 return self._getResultDict(branch, lp_path, suffix, supported_schemes)330 return self._getUrlsForBranch(
331 branch, lp_path, suffix, supported_schemes)
302332
303 def resolve_lp_path(self, path):333 def resolve_lp_path(self, path):
304 """See `IPublicCodehostingAPI`."""334 """See `IPublicCodehostingAPI`."""
305335
=== modified file 'lib/lp/code/xmlrpc/tests/test_branch.py'
--- lib/lp/code/xmlrpc/tests/test_branch.py 2010-09-03 04:22:16 +0000
+++ lib/lp/code/xmlrpc/tests/test_branch.py 2010-09-07 05:51:45 +0000
@@ -59,7 +59,10 @@
59 if lp_path is None:59 if lp_path is None:
60 ssh_branch_path = public_branch_path60 ssh_branch_path = public_branch_path
61 else:61 else:
62 ssh_branch_path = '%s/%s' % (BRANCH_ALIAS_PREFIX, lp_path)62 if lp_path.startswith('~'):
63 ssh_branch_path = lp_path
64 else:
65 ssh_branch_path = '%s/%s' % (BRANCH_ALIAS_PREFIX, lp_path)
63 # This improves the error message if results happens to be a fault.66 # This improves the error message if results happens to be a fault.
64 if isinstance(results, LaunchpadFault):67 if isinstance(results, LaunchpadFault):
65 raise results68 raise results
@@ -70,6 +73,10 @@
70 else:73 else:
71 self.assertEqual('/' + ssh_branch_path, uri.path)74 self.assertEqual('/' + ssh_branch_path, uri.path)
7275
76 def assertOnlyWritableResolves(self, lp_url_path):
77 """Only the bzr+ssh url is returned."""
78 self.assertResolves(lp_url_path, None, lp_url_path)
79
73 def assertFault(self, lp_url_path, expected_fault):80 def assertFault(self, lp_url_path, expected_fault):
74 """Trying to resolve lp_url_path raises the expected fault."""81 """Trying to resolve lp_url_path raises the expected fault."""
75 api = PublicCodehostingAPI(None, None)82 api = PublicCodehostingAPI(None, None)
@@ -122,54 +129,15 @@
122 lp_path='%s/%s' % (product.name, product.development_focus.name)129 lp_path='%s/%s' % (product.name, product.development_focus.name)
123 self.assertResolves(lp_path, trunk.unique_name, lp_path)130 self.assertResolves(lp_path, trunk.unique_name, lp_path)
124131
125 def test_product_doesnt_exist(self):132 def test_target_doesnt_exist(self):
126 # Return a NoSuchProduct fault if the product doesn't exist.133 # The resolver doesn't care if the product exists or not.
127 self.assertFault(134 self.assertOnlyWritableResolves('doesntexist')
128 'doesntexist', faults.NoSuchProduct('doesntexist'))135 self.assertOnlyWritableResolves('doesntexist/trunk')
129 self.assertFault(
130 'doesntexist/trunk', faults.NoSuchProduct('doesntexist'))
131
132 def test_project_group(self):
133 # Resolving lp:///project_group_name' should explain that project
134 # groups don't have default branches.
135 project_group = self.factory.makeProject()
136 fault = self.assertFault(
137 project_group.name, faults.CannotHaveLinkedBranch(project_group))
138 self.assertIn('project group', fault.faultString)
139
140 def test_distro_name(self):
141 # Resolving lp:///distro_name' should explain that distributions don't
142 # have default branches.
143 distro = self.factory.makeDistribution()
144 self.assertFault(distro.name, faults.CannotHaveLinkedBranch(distro))
145
146 def test_distroseries_name(self):
147 # Resolving lp:///distro/series' should explain that distribution
148 # series don't have default branches.
149 series = self.factory.makeDistroSeries()
150 self.assertFault(
151 '%s/%s' % (series.distribution.name, series.name),
152 faults.CannotHaveLinkedBranch(series))
153
154 def test_invalid_product_name(self):
155 # If we get a string that cannot be a name for a product where we
156 # expect the name of a product, we should error appropriately.
157 invalid_name = '_' + self.factory.getUniqueString()
158 self.assertFault(
159 invalid_name,
160 faults.InvalidProductIdentifier(invalid_name))
161
162 def test_invalid_product_name_non_ascii(self):
163 # lp:<non-ascii-string> returns InvalidProductIdentifier with the name
164 # escaped.
165 invalid_name = '_' + NON_ASCII_NAME
166 self.assertFault(
167 invalid_name,
168 faults.InvalidProductIdentifier(urlutils.escape(invalid_name)))
169136
170 def test_product_and_series(self):137 def test_product_and_series(self):
171 # lp:product/series expands to the branch associated with the product138 # lp:product/series expands to the writable alias for product/series
172 # series 'series' on 'product'.139 # and to the branch associated with the product series 'series' on
140 # 'product'.
173 product = self.factory.makeProduct()141 product = self.factory.makeProduct()
174 branch = branch=self.factory.makeProductBranch(product=product)142 branch = branch=self.factory.makeProductBranch(product=product)
175 series = self.factory.makeSeries(product=product, branch=branch)143 series = self.factory.makeSeries(product=product, branch=branch)
@@ -177,79 +145,23 @@
177 self.assertResolves(lp_path, branch.unique_name, lp_path)145 self.assertResolves(lp_path, branch.unique_name, lp_path)
178146
179 def test_development_focus_has_no_branch(self):147 def test_development_focus_has_no_branch(self):
180 # Return a NoLinkedBranch fault if the development focus has no branch148 # A product with no trunk resolves to the writable alias.
181 # associated with it.
182 product = self.factory.makeProduct()149 product = self.factory.makeProduct()
183 self.assertEqual(None, product.development_focus.branch)150 self.assertOnlyWritableResolves(product.name)
184 self.assertFault(product.name, faults.NoLinkedBranch(product))
185151
186 def test_series_has_no_branch(self):152 def test_series_has_no_branch(self):
187 # Return a NoLinkedBranch fault if the series has no branch153 # A series with no branch resolves to the writable alias.
188 # associated with it.
189 series = self.factory.makeSeries(branch=None)154 series = self.factory.makeSeries(branch=None)
190 self.assertFault(155 self.assertOnlyWritableResolves(
191 '%s/%s' % (series.product.name, series.name),156 '%s/%s' % (series.product.name, series.name))
192 faults.NoLinkedBranch(series))
193
194 def test_no_such_product_series(self):
195 # Return a NoSuchProductSeries fault if there is no series of the
196 # given name associated with the product.
197 product = self.factory.makeProduct()
198 self.assertFault(
199 '%s/%s' % (product.name, "doesntexist"),
200 faults.NoSuchProductSeries("doesntexist", product))
201157
202 def test_no_such_product_series_non_ascii(self):158 def test_no_such_product_series_non_ascii(self):
203 # lp:product/<non-ascii-string> returns NoSuchProductSeries with the159 # lp:product/<non-ascii-string> resolves to the branch alias with the
204 # name escaped.160 # name escaped.
205 product = self.factory.makeProduct()161 product = self.factory.makeProduct()
206 self.assertFault(162 lp_path = '%s/%s' % (product.name, NON_ASCII_NAME)
207 '%s/%s' % (product.name, NON_ASCII_NAME),163 escaped_name = urlutils.escape(lp_path)
208 faults.NoSuchProductSeries(164 self.assertResolves(lp_path, None, escaped_name)
209 urlutils.escape(NON_ASCII_NAME), product))
210
211 def test_no_such_distro_series(self):
212 # Return a NoSuchDistroSeries fault if there is no series of the given
213 # name on that distribution.
214 distro = self.factory.makeDistribution()
215 self.assertFault(
216 '%s/doesntexist/whocares' % distro.name,
217 faults.NoSuchDistroSeries("doesntexist"))
218
219 def test_no_such_distro_series_non_ascii(self):
220 # lp:distro/<non-ascii-string>/whatever returns NoSuchDistroSeries
221 # with the name escaped.
222 distro = self.factory.makeDistribution()
223 self.assertFault(
224 '%s/%s/whocares' % (distro.name, NON_ASCII_NAME),
225 faults.NoSuchDistroSeries(urlutils.escape(NON_ASCII_NAME)))
226
227 def test_no_such_source_package(self):
228 # Return a NoSuchSourcePackageName fault if there is no source package
229 # of the given name.
230 distroseries = self.factory.makeDistroRelease()
231 distribution = distroseries.distribution
232 self.assertFault(
233 '%s/%s/doesntexist' % (distribution.name, distroseries.name),
234 faults.NoSuchSourcePackageName('doesntexist'))
235
236 def test_no_such_source_package_non_ascii(self):
237 # lp:distro/series/<non-ascii-name> returns NoSuchSourcePackageName
238 # with the name escaped.
239 distroseries = self.factory.makeDistroRelease()
240 distribution = distroseries.distribution
241 self.assertFault(
242 '%s/%s/%s' % (
243 distribution.name, distroseries.name, NON_ASCII_NAME),
244 faults.NoSuchSourcePackageName(urlutils.escape(NON_ASCII_NAME)))
245
246 def test_no_linked_branch_for_source_package(self):
247 # Return a NoLinkedBranch fault if there's no linked branch for the
248 # sourcepackage.
249 suite_sourcepackage = self.factory.makeSuiteSourcePackage()
250 self.assertFault(
251 suite_sourcepackage.path,
252 faults.NoLinkedBranch(suite_sourcepackage))
253165
254 def test_branch(self):166 def test_branch(self):
255 # The unique name of a branch resolves to the unique name of the167 # The unique name of a branch resolves to the unique name of the
@@ -396,35 +308,27 @@
396 # private branch, and tests are currently running as an anonymous308 # private branch, and tests are currently running as an anonymous
397 # user.309 # user.
398 unique_name = removeSecurityProxy(branch).unique_name310 unique_name = removeSecurityProxy(branch).unique_name
399 self.assertResolves(unique_name, unique_name)311 self.assertOnlyWritableResolves(unique_name)
400312
401 def test_private_branch_on_series(self):313 def test_private_branch_on_series(self):
402 # We resolve invisible branches as if they don't exist. For314 # We resolve private linked branches using the writable alias.
403 # references to product series, this means returning a
404 # NoLinkedBranch fault.
405 #315 #
406 # Removing security proxy because we need to be able to get at316 # Removing security proxy because we need to be able to get at
407 # attributes of a private branch and these tests are running as an317 # attributes of a private branch and these tests are running as an
408 # anonymous user.318 # anonymous user.
409 branch = self.factory.makeAnyBranch(private=True)319 branch = self.factory.makeAnyBranch(private=True)
410 series = self.factory.makeSeries(branch=branch)320 series = self.factory.makeSeries(branch=branch)
411 self.assertFault(321 lp_path='%s/%s' % (series.product.name, series.name)
412 '%s/%s' % (series.product.name, series.name),322 self.assertOnlyWritableResolves(lp_path)
413 faults.NoLinkedBranch(series))
414323
415 def test_private_branch_as_development_focus(self):324 def test_private_branch_as_development_focus(self):
416 # We resolve invisible branches as if they don't exist.325 # We resolve private linked branches using the writable alias.
417 #
418 # References to a product resolve to the branch associated with the
419 # development focus. If that branch is private, other views will
420 # indicate that there is no branch on the development focus. We do the
421 # same.
422 product, trunk = self.makeProdutWithTrunk()326 product, trunk = self.makeProdutWithTrunk()
423 removeSecurityProxy(trunk).private = True327 removeSecurityProxy(trunk).private = True
424 self.assertFault(product.name, faults.NoLinkedBranch(product))328 self.assertOnlyWritableResolves(product.name)
425329
426 def test_private_branch_as_user(self):330 def test_private_branch_as_user(self):
427 # We resolve invisible branches as if they don't exist.331 # We resolve private branches as if they don't exist.
428 #332 #
429 # References to a product resolve to the branch associated with the333 # References to a product resolve to the branch associated with the
430 # development focus. If that branch is private, other views will334 # development focus. If that branch is private, other views will
@@ -436,7 +340,7 @@
436 owner = self.factory.makePerson()340 owner = self.factory.makePerson()
437 branch = self.factory.makeAnyBranch(owner=owner, private=True)341 branch = self.factory.makeAnyBranch(owner=owner, private=True)
438 path = removeSecurityProxy(branch).unique_name342 path = removeSecurityProxy(branch).unique_name
439 self.assertResolves(path, path)343 self.assertOnlyWritableResolves(path)
440344
441 def test_remote_branch(self):345 def test_remote_branch(self):
442 # For remote branches, return results that link to the actual remote346 # For remote branches, return results that link to the actual remote

Subscribers

People subscribed via source and target branches

to status/vote changes: