Merge lp:~thumper/launchpad/xmlrpc-lp-name-resolution into lp:launchpad
- xmlrpc-lp-name-resolution
- Merge into devel
| Status: | Merged |
|---|---|
| Approved by: | Michael Hudson-Doyle on 2010-09-03 |
| Approved revision: | no longer in the source branch. |
| Merged at revision: | 11516 |
| Proposed branch: | lp:~thumper/launchpad/xmlrpc-lp-name-resolution |
| Merge into: | lp:launchpad |
| Diff against target: |
582 lines (+171/-136) 4 files modified
lib/lp/code/interfaces/codehosting.py (+26/-0) lib/lp/code/model/branch.py (+1/-18) lib/lp/code/xmlrpc/branch.py (+42/-14) lib/lp/code/xmlrpc/tests/test_branch.py (+102/-104) |
| To merge this branch: | bzr merge lp:~thumper/launchpad/xmlrpc-lp-name-resolution |
| Related bugs: |
| Reviewer | Review Type | Date Requested | Status |
|---|---|---|---|
| Michael Hudson-Doyle | 2010-09-02 | Approve on 2010-09-03 | |
|
Review via email:
|
|||
Commit Message
Resolve linked branches using the +branch alias over XMLRPC
Description of the Change
This branch changes the way the XMLRPC server resolves the lp: urls where the alias for a branch is used. lp:~user/... are still resolved the same way, as are public http urls.
However for bzr+ssh urls, branches that are linked to series (either distroseries or product series) are now resolved to bzr+ssh:
This is going to be used to allow private trunk branches to use the lp:<project> syntax for accessing branches.
tests:
TestExpandURL
As a drive-by the compose_public_url function was moved into lp.code.
Most of the changes are updates to the tests.
| Tim Penhey (thumper) wrote : | # |
We do have acceptance tests that test that the name we resolve to works for branch access. That landed as part of the branch which supports the +branch alias file access over the VFS.
I'll look at the docstring.
Yes it is a bit hairy, but I was trying to work out how to best provide the URLs we are after without losing the flexibility to add something like the anon-bzr protocol.
Preview Diff
| 1 | === modified file 'lib/lp/code/interfaces/codehosting.py' |
| 2 | --- lib/lp/code/interfaces/codehosting.py 2010-08-20 20:31:18 +0000 |
| 3 | +++ lib/lp/code/interfaces/codehosting.py 2010-09-03 04:25:59 +0000 |
| 4 | @@ -9,17 +9,24 @@ |
| 5 | __all__ = [ |
| 6 | 'BRANCH_ALIAS_PREFIX', |
| 7 | 'BRANCH_TRANSPORT', |
| 8 | + 'compose_public_url', |
| 9 | 'CONTROL_TRANSPORT', |
| 10 | 'ICodehostingAPI', |
| 11 | 'ICodehostingApplication', |
| 12 | 'LAUNCHPAD_ANONYMOUS', |
| 13 | 'LAUNCHPAD_SERVICES', |
| 14 | 'READ_ONLY', |
| 15 | + 'SUPPORTED_SCHEMES', |
| 16 | 'WRITABLE', |
| 17 | ] |
| 18 | |
| 19 | +import os.path |
| 20 | +import urllib |
| 21 | + |
| 22 | +from lazr.uri import URI |
| 23 | from zope.interface import Interface |
| 24 | |
| 25 | +from canonical.config import config |
| 26 | from canonical.launchpad.validators.name import valid_name |
| 27 | from canonical.launchpad.webapp.interfaces import ILaunchpadApplication |
| 28 | |
| 29 | @@ -49,6 +56,9 @@ |
| 30 | # The path prefix for getting at branches via their short name. |
| 31 | BRANCH_ALIAS_PREFIX = '+branch' |
| 32 | |
| 33 | +# The scheme types that are supported for codehosting. |
| 34 | +SUPPORTED_SCHEMES = 'bzr+ssh', 'http' |
| 35 | + |
| 36 | |
| 37 | class ICodehostingApplication(ILaunchpadApplication): |
| 38 | """Branch Puller application root.""" |
| 39 | @@ -170,3 +180,19 @@ |
| 40 | 'path_in_transport' is a path relative to that transport. e.g. |
| 41 | (BRANCH_TRANSPORT, {'id': 3, 'writable': False}, '.bzr/README'). |
| 42 | """ |
| 43 | + |
| 44 | + |
| 45 | +def compose_public_url(scheme, unique_name, suffix=None): |
| 46 | + # Accept sftp as a legacy protocol. |
| 47 | + accepted_schemes = set(SUPPORTED_SCHEMES) |
| 48 | + accepted_schemes.add('sftp') |
| 49 | + assert scheme in accepted_schemes, "Unknown scheme: %s" % scheme |
| 50 | + host = URI(config.codehosting.supermirror_root).host |
| 51 | + if isinstance(unique_name, unicode): |
| 52 | + unique_name = unique_name.encode('utf-8') |
| 53 | + # After quoting and encoding, the path should be perfectly |
| 54 | + # safe as a plain ASCII string, str() just enforces this |
| 55 | + path = '/' + str(urllib.quote(unique_name, safe='/~+')) |
| 56 | + if suffix: |
| 57 | + path = os.path.join(path, suffix) |
| 58 | + return str(URI(scheme=scheme, host=host, path=path)) |
| 59 | |
| 60 | === modified file 'lib/lp/code/model/branch.py' |
| 61 | --- lib/lp/code/model/branch.py 2010-08-27 02:11:36 +0000 |
| 62 | +++ lib/lp/code/model/branch.py 2010-09-03 04:25:59 +0000 |
| 63 | @@ -7,15 +7,12 @@ |
| 64 | __all__ = [ |
| 65 | 'Branch', |
| 66 | 'BranchSet', |
| 67 | - 'compose_public_url', |
| 68 | ] |
| 69 | |
| 70 | from datetime import datetime |
| 71 | -import os.path |
| 72 | |
| 73 | from bzrlib import urlutils |
| 74 | from bzrlib.revision import NULL_REVISION |
| 75 | -from lazr.uri import URI |
| 76 | import pytz |
| 77 | from sqlobject import ( |
| 78 | BoolCol, |
| 79 | @@ -108,6 +105,7 @@ |
| 80 | from lp.code.interfaces.branchnamespace import IBranchNamespacePolicy |
| 81 | from lp.code.interfaces.branchpuller import IBranchPuller |
| 82 | from lp.code.interfaces.branchtarget import IBranchTarget |
| 83 | +from lp.code.interfaces.codehosting import compose_public_url |
| 84 | from lp.code.interfaces.seriessourcepackagebranch import ( |
| 85 | IFindOfficialBranchLinks, |
| 86 | ) |
| 87 | @@ -1324,18 +1322,3 @@ |
| 88 | """ |
| 89 | update_trigger_modified_fields(branch) |
| 90 | send_branch_modified_notifications(branch, event) |
| 91 | - |
| 92 | - |
| 93 | -def compose_public_url(scheme, unique_name, suffix=None): |
| 94 | - # Avoid circular imports. |
| 95 | - from lp.code.xmlrpc.branch import PublicCodehostingAPI |
| 96 | - |
| 97 | - # Accept sftp as a legacy protocol. |
| 98 | - accepted_schemes = set(PublicCodehostingAPI.supported_schemes) |
| 99 | - accepted_schemes.add('sftp') |
| 100 | - assert scheme in accepted_schemes, "Unknown scheme: %s" % scheme |
| 101 | - host = URI(config.codehosting.supermirror_root).host |
| 102 | - path = '/' + urlutils.escape(unique_name) |
| 103 | - if suffix: |
| 104 | - path = os.path.join(path, suffix) |
| 105 | - return str(URI(scheme=scheme, host=host, path=path)) |
| 106 | |
| 107 | === modified file 'lib/lp/code/xmlrpc/branch.py' |
| 108 | --- lib/lp/code/xmlrpc/branch.py 2010-08-20 20:31:18 +0000 |
| 109 | +++ lib/lp/code/xmlrpc/branch.py 2010-09-03 04:25:59 +0000 |
| 110 | @@ -8,8 +8,11 @@ |
| 111 | |
| 112 | __metaclass__ = type |
| 113 | __all__ = [ |
| 114 | - 'BranchSetAPI', 'IBranchSetAPI', 'IPublicCodehostingAPI', |
| 115 | - 'PublicCodehostingAPI'] |
| 116 | + 'BranchSetAPI', |
| 117 | + 'IBranchSetAPI', |
| 118 | + 'IPublicCodehostingAPI', |
| 119 | + 'PublicCodehostingAPI', |
| 120 | + ] |
| 121 | |
| 122 | |
| 123 | from bzrlib import urlutils |
| 124 | @@ -42,6 +45,11 @@ |
| 125 | from lp.code.interfaces.branch import IBranch |
| 126 | from lp.code.interfaces.branchlookup import IBranchLookup |
| 127 | from lp.code.interfaces.branchnamespace import get_branch_namespace |
| 128 | +from lp.code.interfaces.codehosting import ( |
| 129 | + BRANCH_ALIAS_PREFIX, |
| 130 | + compose_public_url, |
| 131 | + SUPPORTED_SCHEMES, |
| 132 | + ) |
| 133 | from lp.registry.interfaces.distroseries import NoSuchDistroSeries |
| 134 | from lp.registry.interfaces.person import ( |
| 135 | IPersonSet, |
| 136 | @@ -171,8 +179,7 @@ |
| 137 | def resolve_lp_path(path): |
| 138 | """Expand the path segment of an lp: URL into a list of branch URLs. |
| 139 | |
| 140 | - This method is added to support Bazaar 0.93. It cannot be removed |
| 141 | - until we stop supporting Bazaar 0.93. |
| 142 | + This method is added to Bazaar in 0.93. |
| 143 | |
| 144 | :return: A dict containing a single 'urls' key that maps to a list of |
| 145 | URLs. Clients should use the first URL in the list that they can |
| 146 | @@ -194,12 +201,25 @@ |
| 147 | |
| 148 | implements(IPublicCodehostingAPI) |
| 149 | |
| 150 | - supported_schemes = 'bzr+ssh', 'http' |
| 151 | - |
| 152 | - def _getResultDict(self, branch, suffix=None, supported_schemes=None): |
| 153 | + def _compose_http_url(unique_name, path, suffix): |
| 154 | + return compose_public_url('http', unique_name, suffix) |
| 155 | + |
| 156 | + def _compose_bzr_ssh_url(unique_name, path, suffix): |
| 157 | + if not path.startswith('~'): |
| 158 | + path = '%s/%s' % (BRANCH_ALIAS_PREFIX, path) |
| 159 | + return compose_public_url('bzr+ssh', path, suffix) |
| 160 | + |
| 161 | + scheme_funcs = { |
| 162 | + 'bzr+ssh': _compose_bzr_ssh_url, |
| 163 | + 'http': _compose_http_url, |
| 164 | + } |
| 165 | + |
| 166 | + def _getResultDict(self, branch, lp_path, suffix=None, |
| 167 | + supported_schemes=None): |
| 168 | """Return a result dict with a list of URLs for the given branch. |
| 169 | |
| 170 | :param branch: A Branch object. |
| 171 | + :param lp_path: The path that was used to traverse to the branch. |
| 172 | :param suffix: The section of the path that follows the branch |
| 173 | specification. |
| 174 | :return: {'urls': [list_of_branch_urls]}. |
| 175 | @@ -210,17 +230,18 @@ |
| 176 | return dict(urls=[branch.url]) |
| 177 | else: |
| 178 | return self._getUniqueNameResultDict( |
| 179 | - branch.unique_name, suffix, supported_schemes) |
| 180 | + branch.unique_name, suffix, supported_schemes, lp_path) |
| 181 | |
| 182 | def _getUniqueNameResultDict(self, unique_name, suffix=None, |
| 183 | - supported_schemes=None): |
| 184 | - from lp.code.model.branch import compose_public_url |
| 185 | + supported_schemes=None, path=None): |
| 186 | if supported_schemes is None: |
| 187 | - supported_schemes = self.supported_schemes |
| 188 | + supported_schemes = SUPPORTED_SCHEMES |
| 189 | + if path is None: |
| 190 | + path = unique_name |
| 191 | result = dict(urls=[]) |
| 192 | for scheme in supported_schemes: |
| 193 | result['urls'].append( |
| 194 | - compose_public_url(scheme, unique_name, suffix)) |
| 195 | + self.scheme_funcs[scheme](unique_name, path, suffix)) |
| 196 | return result |
| 197 | |
| 198 | @return_fault |
| 199 | @@ -233,7 +254,7 @@ |
| 200 | strip_path = path.strip('/') |
| 201 | if strip_path == '': |
| 202 | raise faults.InvalidBranchIdentifier(path) |
| 203 | - supported_schemes = self.supported_schemes |
| 204 | + supported_schemes = SUPPORTED_SCHEMES |
| 205 | hot_products = [product.strip() for product |
| 206 | in config.codehosting.hot_products.split(',')] |
| 207 | if strip_path in hot_products: |
| 208 | @@ -270,7 +291,14 @@ |
| 209 | raise faults.CannotHaveLinkedBranch(e.component) |
| 210 | except InvalidNamespace, e: |
| 211 | raise faults.InvalidBranchUniqueName(urlutils.escape(e.name)) |
| 212 | - return self._getResultDict(branch, suffix, supported_schemes) |
| 213 | + # Reverse engineer the actual lp_path that is used, so we need to |
| 214 | + # remove any suffix that may be there from the strip_path. |
| 215 | + lp_path = strip_path |
| 216 | + if suffix is not None: |
| 217 | + # E.g. 'project/trunk/filename.txt' the suffix is 'filename.txt' |
| 218 | + # we want lp_path to be 'project/trunk'. |
| 219 | + lp_path = lp_path[:-(len(suffix)+1)] |
| 220 | + return self._getResultDict(branch, lp_path, suffix, supported_schemes) |
| 221 | |
| 222 | def resolve_lp_path(self, path): |
| 223 | """See `IPublicCodehostingAPI`.""" |
| 224 | |
| 225 | === modified file 'lib/lp/code/xmlrpc/tests/test_branch.py' |
| 226 | --- lib/lp/code/xmlrpc/tests/test_branch.py 2010-08-20 20:31:18 +0000 |
| 227 | +++ lib/lp/code/xmlrpc/tests/test_branch.py 2010-09-03 04:25:59 +0000 |
| 228 | @@ -3,6 +3,8 @@ |
| 229 | |
| 230 | """Unit tests for the public codehosting API.""" |
| 231 | |
| 232 | +from __future__ import with_statement |
| 233 | + |
| 234 | __metaclass__ = type |
| 235 | __all__ = [] |
| 236 | |
| 237 | @@ -15,17 +17,14 @@ |
| 238 | from lazr.uri import URI |
| 239 | from zope.security.proxy import removeSecurityProxy |
| 240 | |
| 241 | -from canonical.config import config |
| 242 | -from canonical.launchpad.ftests import ( |
| 243 | - login, |
| 244 | - logout, |
| 245 | - ) |
| 246 | from canonical.launchpad.xmlrpc import faults |
| 247 | from canonical.testing import DatabaseFunctionalLayer |
| 248 | from lp.code.enums import BranchType |
| 249 | +from lp.code.interfaces.codehosting import BRANCH_ALIAS_PREFIX |
| 250 | +from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch |
| 251 | from lp.code.xmlrpc.branch import PublicCodehostingAPI |
| 252 | from lp.services.xmlrpc import LaunchpadFault |
| 253 | -from lp.testing import TestCaseWithFactory |
| 254 | +from lp.testing import person_logged_in, TestCaseWithFactory |
| 255 | |
| 256 | |
| 257 | NON_ASCII_NAME = u'nam\N{LATIN SMALL LETTER E WITH ACUTE}' |
| 258 | @@ -36,43 +35,45 @@ |
| 259 | |
| 260 | layer = DatabaseFunctionalLayer |
| 261 | |
| 262 | - def setUp(self): |
| 263 | - """Set up the fixture for these unit tests. |
| 264 | - |
| 265 | - - 'project' is an arbitrary Launchpad project. |
| 266 | - - 'trunk' is a branch on 'project', associated with the development |
| 267 | - focus. |
| 268 | + def makeProdutWithTrunk(self): |
| 269 | + """Make a new project with a trunk hosted branch.""" |
| 270 | + product = self.factory.makeProduct() |
| 271 | + # BranchType is only signficiant insofar as it is not a REMOTE branch. |
| 272 | + trunk = self.factory.makeProductBranch( |
| 273 | + branch_type=BranchType.HOSTED, product=product) |
| 274 | + with person_logged_in(product.owner): |
| 275 | + ICanHasLinkedBranch(product).setBranch(trunk) |
| 276 | + return product, trunk |
| 277 | + |
| 278 | + def assertResolves(self, lp_url_path, public_branch_path, lp_path=None): |
| 279 | + """Assert that `lp_url_path` resolves to the specified paths. |
| 280 | + |
| 281 | + :param public_branch_path: The path that is accessible over http. |
| 282 | + :param lp_path: The short branch alias that will be resolved over |
| 283 | + bzr+ssh. The branch alias prefix is prefixed to this path. |
| 284 | + If it is not set, the bzr+ssh resolved name will be checked |
| 285 | + against the public_branch_path instead. |
| 286 | """ |
| 287 | - TestCaseWithFactory.setUp(self) |
| 288 | - self.api = PublicCodehostingAPI(None, None) |
| 289 | - self.product = self.factory.makeProduct() |
| 290 | - # Associate 'trunk' with the product's development focus. Use |
| 291 | - # removeSecurityProxy so that we can assign directly to branch. |
| 292 | - trunk_series = removeSecurityProxy(self.product).development_focus |
| 293 | - # BranchType is only signficiant insofar as it is not a REMOTE branch. |
| 294 | - trunk_series.branch = ( |
| 295 | - self.factory.makeProductBranch( |
| 296 | - branch_type=BranchType.HOSTED, product=self.product)) |
| 297 | - |
| 298 | - def makePrivateBranch(self, **kwargs): |
| 299 | - """Create an arbitrary private branch using `makeBranch`.""" |
| 300 | - branch = self.factory.makeAnyBranch(**kwargs) |
| 301 | - naked_branch = removeSecurityProxy(branch) |
| 302 | - naked_branch.private = True |
| 303 | - return branch |
| 304 | - |
| 305 | - def assertResolves(self, lp_url_path, unique_name): |
| 306 | - """Assert that `lp_url_path` path expands to `unique_name`.""" |
| 307 | - results = self.api.resolve_lp_path(lp_url_path) |
| 308 | + api = PublicCodehostingAPI(None, None) |
| 309 | + results = api.resolve_lp_path(lp_url_path) |
| 310 | + if lp_path is None: |
| 311 | + ssh_branch_path = public_branch_path |
| 312 | + else: |
| 313 | + ssh_branch_path = '%s/%s' % (BRANCH_ALIAS_PREFIX, lp_path) |
| 314 | # This improves the error message if results happens to be a fault. |
| 315 | if isinstance(results, LaunchpadFault): |
| 316 | raise results |
| 317 | for url in results['urls']: |
| 318 | - self.assertEqual('/' + unique_name, URI(url).path) |
| 319 | + uri = URI(url) |
| 320 | + if uri.scheme == 'http': |
| 321 | + self.assertEqual('/' + public_branch_path, uri.path) |
| 322 | + else: |
| 323 | + self.assertEqual('/' + ssh_branch_path, uri.path) |
| 324 | |
| 325 | def assertFault(self, lp_url_path, expected_fault): |
| 326 | """Trying to resolve lp_url_path raises the expected fault.""" |
| 327 | - fault = self.api.resolve_lp_path(lp_url_path) |
| 328 | + api = PublicCodehostingAPI(None, None) |
| 329 | + fault = api.resolve_lp_path(lp_url_path) |
| 330 | self.assertTrue( |
| 331 | isinstance(fault, xmlrpclib.Fault), |
| 332 | "resolve_lp_path(%r) returned %r, not a Fault." |
| 333 | @@ -87,35 +88,39 @@ |
| 334 | # containing a list of these URLs, with the faster and more featureful |
| 335 | # URLs earlier in the list. We use a dict so we can easily add more |
| 336 | # information in the future. |
| 337 | - trunk = self.product.development_focus.branch |
| 338 | - results = self.api.resolve_lp_path(self.product.name) |
| 339 | + product, trunk = self.makeProdutWithTrunk() |
| 340 | + api = PublicCodehostingAPI(None, None) |
| 341 | + results = api.resolve_lp_path(product.name) |
| 342 | urls = [ |
| 343 | - 'bzr+ssh://bazaar.launchpad.dev/%s' % trunk.unique_name, |
| 344 | + 'bzr+ssh://bazaar.launchpad.dev/+branch/%s' % product.name, |
| 345 | 'http://bazaar.launchpad.dev/%s' % trunk.unique_name] |
| 346 | self.assertEqual(dict(urls=urls), results) |
| 347 | |
| 348 | def test_resultDictForHotProduct(self): |
| 349 | # If 'project-name' is in the config.codehosting.hot_products list, |
| 350 | # lp:project-name will only resolve to the http url. |
| 351 | - config.push( |
| 352 | - 'hot_product', |
| 353 | - '[codehosting]\nhot_products: %s '% self.product.name) |
| 354 | - self.addCleanup(config.pop, 'hot_product') |
| 355 | - trunk = self.product.development_focus.branch |
| 356 | - results = self.api.resolve_lp_path(self.product.name) |
| 357 | + product, trunk = self.makeProdutWithTrunk() |
| 358 | + self.pushConfig('codehosting', hot_products=product.name) |
| 359 | + api = PublicCodehostingAPI(None, None) |
| 360 | + results = api.resolve_lp_path(product.name) |
| 361 | http_url = 'http://bazaar.launchpad.dev/%s' % trunk.unique_name |
| 362 | self.assertEqual(dict(urls=[http_url]), results) |
| 363 | |
| 364 | def test_product_only(self): |
| 365 | # lp:product expands to the branch associated with development focus |
| 366 | - # of the product. |
| 367 | - trunk = self.product.development_focus.branch |
| 368 | - self.assertResolves(self.product.name, trunk.unique_name) |
| 369 | - trunk_series = removeSecurityProxy(self.product).development_focus |
| 370 | - trunk_series.branch = self.factory.makeProductBranch( |
| 371 | - branch_type=BranchType.HOSTED, product=self.product) |
| 372 | - self.assertResolves( |
| 373 | - self.product.name, trunk_series.branch.unique_name) |
| 374 | + # of the product for the anonymous public access, just to the aliased |
| 375 | + # short name for bzr+ssh access. |
| 376 | + product, trunk = self.makeProdutWithTrunk() |
| 377 | + lp_path = product.name |
| 378 | + self.assertResolves(lp_path, trunk.unique_name, lp_path) |
| 379 | + |
| 380 | + def test_product_explicit_dev_series(self): |
| 381 | + # lp:product/development_focus expands to the branch associated with |
| 382 | + # development focus of the product for the anonymous public access, |
| 383 | + # just to the aliased short name for bzr+ssh access. |
| 384 | + product, trunk = self.makeProdutWithTrunk() |
| 385 | + lp_path='%s/%s' % (product.name, product.development_focus.name) |
| 386 | + self.assertResolves(lp_path, trunk.unique_name, lp_path) |
| 387 | |
| 388 | def test_product_doesnt_exist(self): |
| 389 | # Return a NoSuchProduct fault if the product doesn't exist. |
| 390 | @@ -165,18 +170,11 @@ |
| 391 | def test_product_and_series(self): |
| 392 | # lp:product/series expands to the branch associated with the product |
| 393 | # series 'series' on 'product'. |
| 394 | - series = self.factory.makeSeries( |
| 395 | - product=self.product, |
| 396 | - branch=self.factory.makeProductBranch(product=self.product)) |
| 397 | - self.assertResolves( |
| 398 | - '%s/%s' % (self.product.name, series.name), |
| 399 | - series.branch.unique_name) |
| 400 | - |
| 401 | - # We can also use product/series notation to reach trunk. |
| 402 | - self.assertResolves( |
| 403 | - '%s/%s' % (self.product.name, |
| 404 | - self.product.development_focus.name), |
| 405 | - self.product.development_focus.branch.unique_name) |
| 406 | + product = self.factory.makeProduct() |
| 407 | + branch = branch=self.factory.makeProductBranch(product=product) |
| 408 | + series = self.factory.makeSeries(product=product, branch=branch) |
| 409 | + lp_path = '%s/%s' % (product.name, series.name) |
| 410 | + self.assertResolves(lp_path, branch.unique_name, lp_path) |
| 411 | |
| 412 | def test_development_focus_has_no_branch(self): |
| 413 | # Return a NoLinkedBranch fault if the development focus has no branch |
| 414 | @@ -196,17 +194,19 @@ |
| 415 | def test_no_such_product_series(self): |
| 416 | # Return a NoSuchProductSeries fault if there is no series of the |
| 417 | # given name associated with the product. |
| 418 | + product = self.factory.makeProduct() |
| 419 | self.assertFault( |
| 420 | - '%s/%s' % (self.product.name, "doesntexist"), |
| 421 | - faults.NoSuchProductSeries("doesntexist", self.product)) |
| 422 | + '%s/%s' % (product.name, "doesntexist"), |
| 423 | + faults.NoSuchProductSeries("doesntexist", product)) |
| 424 | |
| 425 | def test_no_such_product_series_non_ascii(self): |
| 426 | # lp:product/<non-ascii-string> returns NoSuchProductSeries with the |
| 427 | # name escaped. |
| 428 | + product = self.factory.makeProduct() |
| 429 | self.assertFault( |
| 430 | - '%s/%s' % (self.product.name, NON_ASCII_NAME), |
| 431 | + '%s/%s' % (product.name, NON_ASCII_NAME), |
| 432 | faults.NoSuchProductSeries( |
| 433 | - urlutils.escape(NON_ASCII_NAME), self.product)) |
| 434 | + urlutils.escape(NON_ASCII_NAME), product)) |
| 435 | |
| 436 | def test_no_such_distro_series(self): |
| 437 | # Return a NoSuchDistroSeries fault if there is no series of the given |
| 438 | @@ -254,34 +254,37 @@ |
| 439 | def test_branch(self): |
| 440 | # The unique name of a branch resolves to the unique name of the |
| 441 | # branch. |
| 442 | - arbitrary_branch = self.factory.makeAnyBranch() |
| 443 | - self.assertResolves( |
| 444 | - arbitrary_branch.unique_name, arbitrary_branch.unique_name) |
| 445 | - trunk = self.product.development_focus.branch |
| 446 | + branch = self.factory.makeAnyBranch() |
| 447 | + self.assertResolves(branch.unique_name, branch.unique_name) |
| 448 | + |
| 449 | + def test_trunk_accessed_as_branch(self): |
| 450 | + # A branch that is the development focus for any product can also be |
| 451 | + # accessed through the branch's unique_name. |
| 452 | + _ignored, trunk = self.makeProdutWithTrunk() |
| 453 | self.assertResolves(trunk.unique_name, trunk.unique_name) |
| 454 | |
| 455 | def test_mirrored_branch(self): |
| 456 | # The unique name of a mirrored branch resolves to the unique name of |
| 457 | # the branch. |
| 458 | - arbitrary_branch = self.factory.makeAnyBranch( |
| 459 | - branch_type=BranchType.MIRRORED) |
| 460 | - self.assertResolves( |
| 461 | - arbitrary_branch.unique_name, arbitrary_branch.unique_name) |
| 462 | + branch = self.factory.makeAnyBranch(branch_type=BranchType.MIRRORED) |
| 463 | + self.assertResolves(branch.unique_name, branch.unique_name) |
| 464 | |
| 465 | def test_no_such_branch_product(self): |
| 466 | # Resolve paths to branches even if there is no branch of that name. |
| 467 | # We do this so that users can push new branches to lp: URLs. |
| 468 | owner = self.factory.makePerson() |
| 469 | + product = self.factory.makeProduct() |
| 470 | nonexistent_branch = '~%s/%s/doesntexist' % ( |
| 471 | - owner.name, self.product.name) |
| 472 | + owner.name, product.name) |
| 473 | self.assertResolves(nonexistent_branch, nonexistent_branch) |
| 474 | |
| 475 | def test_no_such_branch_product_non_ascii(self): |
| 476 | # A path to a branch that contains non ascii characters will never |
| 477 | # find a branch, but it still resolves rather than erroring. |
| 478 | owner = self.factory.makePerson() |
| 479 | + product = self.factory.makeProduct() |
| 480 | nonexistent_branch = u'~%s/%s/%s' % ( |
| 481 | - owner.name, self.product.name, NON_ASCII_NAME) |
| 482 | + owner.name, product.name, NON_ASCII_NAME) |
| 483 | self.assertResolves( |
| 484 | nonexistent_branch, urlutils.escape(nonexistent_branch)) |
| 485 | |
| 486 | @@ -291,8 +294,7 @@ |
| 487 | # the '+junk' project, which doesn't actually exist. |
| 488 | owner = self.factory.makePerson() |
| 489 | nonexistent_branch = '~%s/+junk/doesntexist' % owner.name |
| 490 | - self.assertResolves( |
| 491 | - nonexistent_branch, urlutils.escape(nonexistent_branch)) |
| 492 | + self.assertResolves(nonexistent_branch, nonexistent_branch) |
| 493 | |
| 494 | def test_no_such_branch_package(self): |
| 495 | # Resolve paths to package branches even if there's no branch of that |
| 496 | @@ -374,26 +376,26 @@ |
| 497 | def test_trailing_slashes(self): |
| 498 | # Trailing slashes are trimmed. |
| 499 | # Trailing slashes on lp:product// |
| 500 | - trunk = self.product.development_focus.branch |
| 501 | - self.assertResolves(self.product.name + '/', trunk.unique_name) |
| 502 | - self.assertResolves(self.product.name + '//', trunk.unique_name) |
| 503 | + product, trunk = self.makeProdutWithTrunk() |
| 504 | + self.assertResolves( |
| 505 | + product.name + '/', trunk.unique_name, product.name) |
| 506 | + self.assertResolves( |
| 507 | + product.name + '//', trunk.unique_name, product.name) |
| 508 | |
| 509 | # Trailing slashes on lp:~owner/product/branch// |
| 510 | - arbitrary_branch = self.factory.makeAnyBranch() |
| 511 | - self.assertResolves( |
| 512 | - arbitrary_branch.unique_name + '/', arbitrary_branch.unique_name) |
| 513 | - self.assertResolves( |
| 514 | - arbitrary_branch.unique_name + '//', arbitrary_branch.unique_name) |
| 515 | + branch = self.factory.makeAnyBranch() |
| 516 | + self.assertResolves(branch.unique_name + '/', branch.unique_name) |
| 517 | + self.assertResolves(branch.unique_name + '//', branch.unique_name) |
| 518 | |
| 519 | def test_private_branch(self): |
| 520 | # Invisible branches are resolved as if they didn't exist, so that we |
| 521 | # reveal the least possile amount of information about them. |
| 522 | # For fully specified branch names, this means resolving the lp url. |
| 523 | - arbitrary_branch = self.makePrivateBranch() |
| 524 | + branch = self.factory.makeAnyBranch(private=True) |
| 525 | # Removing security proxy to get at the unique_name attribute of a |
| 526 | # private branch, and tests are currently running as an anonymous |
| 527 | # user. |
| 528 | - unique_name = removeSecurityProxy(arbitrary_branch).unique_name |
| 529 | + unique_name = removeSecurityProxy(branch).unique_name |
| 530 | self.assertResolves(unique_name, unique_name) |
| 531 | |
| 532 | def test_private_branch_on_series(self): |
| 533 | @@ -404,7 +406,7 @@ |
| 534 | # Removing security proxy because we need to be able to get at |
| 535 | # attributes of a private branch and these tests are running as an |
| 536 | # anonymous user. |
| 537 | - branch = removeSecurityProxy(self.makePrivateBranch()) |
| 538 | + branch = self.factory.makeAnyBranch(private=True) |
| 539 | series = self.factory.makeSeries(branch=branch) |
| 540 | self.assertFault( |
| 541 | '%s/%s' % (series.product.name, series.name), |
| 542 | @@ -417,11 +419,9 @@ |
| 543 | # development focus. If that branch is private, other views will |
| 544 | # indicate that there is no branch on the development focus. We do the |
| 545 | # same. |
| 546 | - trunk = self.product.development_focus.branch |
| 547 | - naked_trunk = removeSecurityProxy(trunk) |
| 548 | - naked_trunk.private = True |
| 549 | - self.assertFault( |
| 550 | - self.product.name, faults.NoLinkedBranch(self.product)) |
| 551 | + product, trunk = self.makeProdutWithTrunk() |
| 552 | + removeSecurityProxy(trunk).private = True |
| 553 | + self.assertFault(product.name, faults.NoLinkedBranch(product)) |
| 554 | |
| 555 | def test_private_branch_as_user(self): |
| 556 | # We resolve invisible branches as if they don't exist. |
| 557 | @@ -433,19 +433,17 @@ |
| 558 | # |
| 559 | # Create the owner explicitly so that we can get its email without |
| 560 | # resorting to removeSecurityProxy. |
| 561 | - email = self.factory.getUniqueEmailAddress() |
| 562 | - arbitrary_branch = self.makePrivateBranch( |
| 563 | - owner=self.factory.makePerson(email=email)) |
| 564 | - login(email) |
| 565 | - self.addCleanup(logout) |
| 566 | - self.assertResolves( |
| 567 | - arbitrary_branch.unique_name, arbitrary_branch.unique_name) |
| 568 | + owner = self.factory.makePerson() |
| 569 | + branch = self.factory.makeAnyBranch(owner=owner, private=True) |
| 570 | + path = removeSecurityProxy(branch).unique_name |
| 571 | + self.assertResolves(path, path) |
| 572 | |
| 573 | def test_remote_branch(self): |
| 574 | # For remote branches, return results that link to the actual remote |
| 575 | # branch URL. |
| 576 | branch = self.factory.makeAnyBranch(branch_type=BranchType.REMOTE) |
| 577 | - result = self.api.resolve_lp_path(branch.unique_name) |
| 578 | + api = PublicCodehostingAPI(None, None) |
| 579 | + result = api.resolve_lp_path(branch.unique_name) |
| 580 | self.assertEqual([branch.url], result['urls']) |
| 581 | |
| 582 | def test_remote_branch_no_url(self): |

Hi,
I'm a bit confused about the whole supported_schemes stuff, it seems overly hairy. But it's not new.
The docstring of assertRaises has become a bit garbled, can you have a proof read and fix it up?
Do we have a codehosting acceptance test that accesses a branch by its lp: urls? Maybe we should.
Otherwise all fine. People will be very happy about this :-)
Cheers,
mwh