Merge lp:~mwhudson/launchpad/cross-product-spec-links-bug-3552 into lp:launchpad
- cross-product-spec-links-bug-3552
- Merge into devel
Proposed by
Michael Hudson-Doyle
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Michael Hudson-Doyle | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 11459 | ||||
Proposed branch: | lp:~mwhudson/launchpad/cross-product-spec-links-bug-3552 | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
458 lines (+238/-41) 6 files modified
lib/lp/blueprints/browser/specificationdependency.py (+3/-1) lib/lp/blueprints/browser/tests/test_specificationdependency.py (+33/-0) lib/lp/blueprints/vocabularies/specificationdependency.py (+81/-25) lib/lp/blueprints/vocabularies/tests/specificationdepcandidates.txt (+6/-8) lib/lp/blueprints/vocabularies/tests/test_specificationdependency.py (+111/-5) lib/lp/testing/factory.py (+4/-2) |
||||
To merge this branch: | bzr merge lp:~mwhudson/launchpad/cross-product-spec-links-bug-3552 | ||||
Related bugs: |
|
||||
Related blueprints: |
Cross-project blueprint references
(Undefined)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Tim Penhey (community) | Approve | ||
Review via email: mp+33866@code.launchpad.net |
Commit message
Allow the creation of cross-project blueprint dependencies (a present from Linaro!)
Description of the change
Hi,
This branch changes the way the vocabulary for specification dependency candidates works. In particular, it allows you to create cross-pillar dependencies by entering the URL of a specification.
I had a pre-imp chat with Tim.
Cheers,
mwh
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/blueprints/browser/specificationdependency.py' | |||
2 | --- lib/lp/blueprints/browser/specificationdependency.py 2010-08-26 03:02:29 +0000 | |||
3 | +++ lib/lp/blueprints/browser/specificationdependency.py 2010-08-27 04:52:44 +0000 | |||
4 | @@ -37,7 +37,9 @@ | |||
5 | 37 | "If another blueprint needs to be fully implemented " | 37 | "If another blueprint needs to be fully implemented " |
6 | 38 | "before this feature can be started, then specify that " | 38 | "before this feature can be started, then specify that " |
7 | 39 | "dependency here so Launchpad knows about it and can " | 39 | "dependency here so Launchpad knows about it and can " |
9 | 40 | "give you an accurate project plan.")) | 40 | "give you an accurate project plan. You can enter the " |
10 | 41 | "name of a blueprint that has the same target, or the " | ||
11 | 42 | "URL of any blueprint.")) | ||
12 | 41 | 43 | ||
13 | 42 | 44 | ||
14 | 43 | class SpecificationDependencyAddView(LaunchpadFormView): | 45 | class SpecificationDependencyAddView(LaunchpadFormView): |
15 | 44 | 46 | ||
16 | === added file 'lib/lp/blueprints/browser/tests/test_specificationdependency.py' | |||
17 | --- lib/lp/blueprints/browser/tests/test_specificationdependency.py 1970-01-01 00:00:00 +0000 | |||
18 | +++ lib/lp/blueprints/browser/tests/test_specificationdependency.py 2010-08-27 04:52:44 +0000 | |||
19 | @@ -0,0 +1,33 @@ | |||
20 | 1 | # Copyright 2010 Canonical Ltd. This software is licensed under the | ||
21 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
22 | 3 | |||
23 | 4 | """Tests for the specification dependency views. | ||
24 | 5 | |||
25 | 6 | There are also tests in lp/blueprints/stories/blueprints/xx-dependencies.txt. | ||
26 | 7 | """ | ||
27 | 8 | |||
28 | 9 | __metaclass__ = type | ||
29 | 10 | |||
30 | 11 | import unittest | ||
31 | 12 | |||
32 | 13 | from canonical.launchpad.webapp import canonical_url | ||
33 | 14 | from canonical.testing import DatabaseFunctionalLayer | ||
34 | 15 | from lp.testing import BrowserTestCase | ||
35 | 16 | |||
36 | 17 | |||
37 | 18 | class TestAddDependency(BrowserTestCase): | ||
38 | 19 | layer = DatabaseFunctionalLayer | ||
39 | 20 | |||
40 | 21 | def test_add_dependency_by_url(self): | ||
41 | 22 | # It is possible to use the URL of a specification in the "Depends On" | ||
42 | 23 | # field of the form to add a dependency to a spec. | ||
43 | 24 | spec = self.factory.makeSpecification(owner=self.user) | ||
44 | 25 | dependency = self.factory.makeSpecification() | ||
45 | 26 | browser = self.getViewBrowser(spec, '+linkdependency') | ||
46 | 27 | browser.getControl('Depends On').value = canonical_url(dependency) | ||
47 | 28 | browser.getControl('Continue').click() | ||
48 | 29 | self.assertIn(dependency, spec.dependencies) | ||
49 | 30 | |||
50 | 31 | |||
51 | 32 | def test_suite(): | ||
52 | 33 | return unittest.TestLoader().loadTestsFromName(__name__) | ||
53 | 0 | 34 | ||
54 | === modified file 'lib/lp/blueprints/vocabularies/specificationdependency.py' | |||
55 | --- lib/lp/blueprints/vocabularies/specificationdependency.py 2010-08-26 03:30:31 +0000 | |||
56 | +++ lib/lp/blueprints/vocabularies/specificationdependency.py 2010-08-27 04:52:44 +0000 | |||
57 | @@ -6,11 +6,16 @@ | |||
58 | 6 | __metaclass__ = type | 6 | __metaclass__ = type |
59 | 7 | __all__ = ['SpecificationDepCandidatesVocabulary'] | 7 | __all__ = ['SpecificationDepCandidatesVocabulary'] |
60 | 8 | 8 | ||
61 | 9 | from zope.component import getUtility | ||
62 | 9 | from zope.interface import implements | 10 | from zope.interface import implements |
63 | 10 | from zope.schema.vocabulary import SimpleTerm | 11 | from zope.schema.vocabulary import SimpleTerm |
64 | 11 | 12 | ||
65 | 12 | from canonical.database.sqlbase import quote_like | 13 | from canonical.database.sqlbase import quote_like |
66 | 13 | from canonical.launchpad.helpers import shortlist | 14 | from canonical.launchpad.helpers import shortlist |
67 | 15 | from canonical.launchpad.webapp import ( | ||
68 | 16 | canonical_url, | ||
69 | 17 | urlparse, | ||
70 | 18 | ) | ||
71 | 14 | from canonical.launchpad.webapp.vocabulary import ( | 19 | from canonical.launchpad.webapp.vocabulary import ( |
72 | 15 | CountableIterator, | 20 | CountableIterator, |
73 | 16 | IHugeVocabulary, | 21 | IHugeVocabulary, |
74 | @@ -19,15 +24,26 @@ | |||
75 | 19 | 24 | ||
76 | 20 | from lp.blueprints.interfaces.specification import SpecificationFilter | 25 | from lp.blueprints.interfaces.specification import SpecificationFilter |
77 | 21 | from lp.blueprints.model.specification import Specification | 26 | from lp.blueprints.model.specification import Specification |
79 | 22 | 27 | from lp.registry.interfaces.pillar import IPillarNameSet | |
80 | 23 | 28 | ||
81 | 24 | class SpecificationDepCandidatesVocabulary(SQLObjectVocabularyBase): | 29 | class SpecificationDepCandidatesVocabulary(SQLObjectVocabularyBase): |
82 | 25 | """Specifications that could be dependencies of this spec. | 30 | """Specifications that could be dependencies of this spec. |
83 | 26 | 31 | ||
88 | 27 | This includes only those specs that are not blocked by this spec | 32 | This includes only those specs that are not blocked by this spec (directly |
89 | 28 | (directly or indirectly), unless they are already dependencies. | 33 | or indirectly), unless they are already dependencies. |
90 | 29 | 34 | ||
91 | 30 | The current spec is not included. | 35 | This vocabulary has a bit of a split personality. |
92 | 36 | |||
93 | 37 | Tokens are *either*: | ||
94 | 38 | |||
95 | 39 | - the name of a spec, in which case it must be a spec on the same target | ||
96 | 40 | as the context, or | ||
97 | 41 | - the full URL of the spec, in which case it can be any spec at all. | ||
98 | 42 | |||
99 | 43 | For the purposes of enumeration and searching we only consider the first | ||
100 | 44 | sort of spec for now. The URL form of token only matches precisely, | ||
101 | 45 | searching only looks for specs on the current target if the search term is | ||
102 | 46 | not a URL. | ||
103 | 31 | """ | 47 | """ |
104 | 32 | 48 | ||
105 | 33 | implements(IHugeVocabulary) | 49 | implements(IHugeVocabulary) |
106 | @@ -36,8 +52,8 @@ | |||
107 | 36 | _orderBy = 'name' | 52 | _orderBy = 'name' |
108 | 37 | displayname = 'Select a blueprint' | 53 | displayname = 'Select a blueprint' |
109 | 38 | 54 | ||
112 | 39 | def _filter_specs(self, specs): | 55 | def _is_valid_candidate(self, spec, check_target=False): |
113 | 40 | """Filter `specs` to remove invalid candidates. | 56 | """Is `spec` a valid candidate spec for self.context? |
114 | 41 | 57 | ||
115 | 42 | Invalid candidates are: | 58 | Invalid candidates are: |
116 | 43 | 59 | ||
117 | @@ -47,27 +63,64 @@ | |||
118 | 47 | 63 | ||
119 | 48 | Preventing the last category prevents loops in the dependency graph. | 64 | Preventing the last category prevents loops in the dependency graph. |
120 | 49 | """ | 65 | """ |
121 | 66 | if check_target and spec.target != self.context.target: | ||
122 | 67 | return False | ||
123 | 68 | return spec != self.context and spec not in self.context.all_blocked | ||
124 | 69 | |||
125 | 70 | def _filter_specs(self, specs, check_target=False): | ||
126 | 71 | """Filter `specs` to remove invalid candidates. | ||
127 | 72 | |||
128 | 73 | See `_is_valid_candidate` for what an invalid candidate is. | ||
129 | 74 | """ | ||
130 | 50 | # XXX intellectronica 2007-07-05: is 100 a reasonable count before | 75 | # XXX intellectronica 2007-07-05: is 100 a reasonable count before |
131 | 51 | # starting to warn? | 76 | # starting to warn? |
137 | 52 | speclist = shortlist(specs, 100) | 77 | return [spec for spec in shortlist(specs, 100) |
138 | 53 | return [spec for spec in speclist | 78 | if self._is_valid_candidate(spec, check_target)] |
134 | 54 | if (spec != self.context and | ||
135 | 55 | spec.target == self.context.target | ||
136 | 56 | and spec not in self.context.all_blocked)] | ||
139 | 57 | 79 | ||
140 | 58 | def toTerm(self, obj): | 80 | def toTerm(self, obj): |
142 | 59 | return SimpleTerm(obj, obj.name, obj.title) | 81 | if obj.target == self.context.target: |
143 | 82 | token = obj.name | ||
144 | 83 | else: | ||
145 | 84 | token = canonical_url(obj) | ||
146 | 85 | return SimpleTerm(obj, token, obj.title) | ||
147 | 86 | |||
148 | 87 | def _spec_from_url(self, url): | ||
149 | 88 | """If `url` is the URL of a specification, return it. | ||
150 | 89 | |||
151 | 90 | This implementation is a little fuzzy and will return specs for URLs | ||
152 | 91 | that, for example, don't have the host name right. This seems | ||
153 | 92 | unlikely to cause confusion in practice, and being too anal probably | ||
154 | 93 | would be confusing (e.g. not accepting edge URLs on lpnet). | ||
155 | 94 | """ | ||
156 | 95 | scheme, netloc, path, params, args, fragment = urlparse(url) | ||
157 | 96 | if not scheme or not netloc: | ||
158 | 97 | # Not enough like a URL | ||
159 | 98 | return None | ||
160 | 99 | path_segments = path.strip('/').split('/') | ||
161 | 100 | if len(path_segments) != 3: | ||
162 | 101 | # Can't be a spec url | ||
163 | 102 | return None | ||
164 | 103 | pillar_name, plus_spec, spec_name = path_segments | ||
165 | 104 | if plus_spec != '+spec': | ||
166 | 105 | # Can't be a spec url | ||
167 | 106 | return None | ||
168 | 107 | pillar = getUtility(IPillarNameSet).getByName( | ||
169 | 108 | pillar_name, ignore_inactive=True) | ||
170 | 109 | if pillar is None: | ||
171 | 110 | return None | ||
172 | 111 | return pillar.getSpecification(spec_name) | ||
173 | 60 | 112 | ||
174 | 61 | def getTermByToken(self, token): | 113 | def getTermByToken(self, token): |
175 | 62 | """See `zope.schema.interfaces.IVocabularyTokenized`. | 114 | """See `zope.schema.interfaces.IVocabularyTokenized`. |
176 | 63 | 115 | ||
178 | 64 | The tokens for specifications are just the name of the spec. | 116 | The tokens for specifications are either the name of a spec on the |
179 | 117 | same target or a URL for a spec. | ||
180 | 65 | """ | 118 | """ |
186 | 66 | spec = self.context.target.getSpecification(token) | 119 | spec = self._spec_from_url(token) |
187 | 67 | if spec is not None: | 120 | if spec is None: |
188 | 68 | filtered = self._filter_specs([spec]) | 121 | spec = self.context.target.getSpecification(token) |
189 | 69 | if len(filtered) > 0: | 122 | if spec and self._is_valid_candidate(spec): |
190 | 70 | return self.toTerm(filtered[0]) | 123 | return self.toTerm(spec) |
191 | 71 | raise LookupError(token) | 124 | raise LookupError(token) |
192 | 72 | 125 | ||
193 | 73 | def search(self, query): | 126 | def search(self, query): |
194 | @@ -79,6 +132,9 @@ | |||
195 | 79 | """ | 132 | """ |
196 | 80 | if not query: | 133 | if not query: |
197 | 81 | return CountableIterator(0, []) | 134 | return CountableIterator(0, []) |
198 | 135 | spec = self._spec_from_url(query) | ||
199 | 136 | if spec is not None and self._is_valid_candidate(spec): | ||
200 | 137 | return CountableIterator(1, [spec]) | ||
201 | 82 | quoted_query = quote_like(query) | 138 | quoted_query = quote_like(query) |
202 | 83 | sql_query = (""" | 139 | sql_query = (""" |
203 | 84 | (Specification.name LIKE %s OR | 140 | (Specification.name LIKE %s OR |
204 | @@ -87,18 +143,18 @@ | |||
205 | 87 | """ | 143 | """ |
206 | 88 | % (quoted_query, quoted_query, quoted_query)) | 144 | % (quoted_query, quoted_query, quoted_query)) |
207 | 89 | all_specs = Specification.select(sql_query, orderBy=self._orderBy) | 145 | all_specs = Specification.select(sql_query, orderBy=self._orderBy) |
209 | 90 | candidate_specs = self._filter_specs(all_specs) | 146 | candidate_specs = self._filter_specs(all_specs, check_target=True) |
210 | 91 | return CountableIterator(len(candidate_specs), candidate_specs) | 147 | return CountableIterator(len(candidate_specs), candidate_specs) |
211 | 92 | 148 | ||
212 | 93 | @property | 149 | @property |
213 | 94 | def _all_specs(self): | 150 | def _all_specs(self): |
214 | 95 | return self.context.target.specifications( | 151 | return self.context.target.specifications( |
217 | 96 | filter=[SpecificationFilter.ALL], | 152 | filter=[SpecificationFilter.ALL], prejoin_people=False) |
216 | 97 | prejoin_people=False) | ||
218 | 98 | 153 | ||
219 | 99 | def __iter__(self): | 154 | def __iter__(self): |
222 | 100 | return (self.toTerm(spec) | 155 | return ( |
223 | 101 | for spec in self._filter_specs(self._all_specs)) | 156 | self.toTerm(spec) for spec in self._filter_specs(self._all_specs)) |
224 | 102 | 157 | ||
225 | 103 | def __contains__(self, obj): | 158 | def __contains__(self, obj): |
227 | 104 | return obj in self._all_specs and len(self._filter_specs([obj])) > 0 | 159 | return self._is_valid_candidate(obj) |
228 | 160 | |||
229 | 105 | 161 | ||
230 | === modified file 'lib/lp/blueprints/vocabularies/tests/specificationdepcandidates.txt' | |||
231 | --- lib/lp/blueprints/vocabularies/tests/specificationdepcandidates.txt 2010-08-25 05:39:32 +0000 | |||
232 | +++ lib/lp/blueprints/vocabularies/tests/specificationdepcandidates.txt 2010-08-27 04:52:44 +0000 | |||
233 | @@ -22,26 +22,24 @@ | |||
234 | 22 | >>> sorted([spec.name for spec in specced_product.specifications()]) | 22 | >>> sorted([spec.name for spec in specced_product.specifications()]) |
235 | 23 | [u'spec-a', u'spec-b', u'spec-c'] | 23 | [u'spec-a', u'spec-b', u'spec-c'] |
236 | 24 | 24 | ||
239 | 25 | The dependency candidates for spec_a are all blueprints for | 25 | |
240 | 26 | specced_product except for spec_a itself. | 26 | Iterating over the vocabulary gives all blueprints for specced_product |
241 | 27 | except for spec_a itself. | ||
242 | 27 | 28 | ||
243 | 28 | >>> vocab = vocabulary_registry.get( | 29 | >>> vocab = vocabulary_registry.get( |
244 | 29 | ... spec_a, "SpecificationDepCandidates") | 30 | ... spec_a, "SpecificationDepCandidates") |
245 | 30 | >>> sorted([term.value.name for term in vocab]) | 31 | >>> sorted([term.value.name for term in vocab]) |
246 | 31 | [u'spec-b', u'spec-c'] | 32 | [u'spec-b', u'spec-c'] |
247 | 32 | 33 | ||
250 | 33 | Dependency candidate come only from the same product of the blueprint | 34 | Blueprints for other targets are considered to be 'in' the vocabulary |
251 | 34 | they depend on. | 35 | though. |
252 | 35 | 36 | ||
253 | 36 | >>> unrelated_spec = factory.makeSpecification( | 37 | >>> unrelated_spec = factory.makeSpecification( |
254 | 37 | ... product=factory.makeProduct()) | 38 | ... product=factory.makeProduct()) |
255 | 38 | >>> vocab = vocabulary_registry.get( | 39 | >>> vocab = vocabulary_registry.get( |
256 | 39 | ... spec_a, "SpecificationDepCandidates") | 40 | ... spec_a, "SpecificationDepCandidates") |
257 | 40 | >>> unrelated_spec in vocab | 41 | >>> unrelated_spec in vocab |
262 | 41 | False | 42 | True |
259 | 42 | >>> [term.value.product for term in vocab | ||
260 | 43 | ... if term.value.product != specced_product] | ||
261 | 44 | [] | ||
263 | 45 | 43 | ||
264 | 46 | We mark spec_b as a dependency of spec_a and spec_c as a dependency of | 44 | We mark spec_b as a dependency of spec_a and spec_c as a dependency of |
265 | 47 | spec_b. | 45 | spec_b. |
266 | 48 | 46 | ||
267 | === modified file 'lib/lp/blueprints/vocabularies/tests/test_specificationdependency.py' | |||
268 | --- lib/lp/blueprints/vocabularies/tests/test_specificationdependency.py 2010-08-26 22:07:41 +0000 | |||
269 | +++ lib/lp/blueprints/vocabularies/tests/test_specificationdependency.py 2010-08-27 04:52:44 +0000 | |||
270 | @@ -10,6 +10,7 @@ | |||
271 | 10 | 10 | ||
272 | 11 | from zope.schema.vocabulary import getVocabularyRegistry | 11 | from zope.schema.vocabulary import getVocabularyRegistry |
273 | 12 | 12 | ||
274 | 13 | from canonical.launchpad.webapp import canonical_url | ||
275 | 13 | from canonical.testing import DatabaseFunctionalLayer | 14 | from canonical.testing import DatabaseFunctionalLayer |
276 | 14 | 15 | ||
277 | 15 | from lp.testing import TestCaseWithFactory | 16 | from lp.testing import TestCaseWithFactory |
278 | @@ -24,7 +25,7 @@ | |||
279 | 24 | return getVocabularyRegistry().get( | 25 | return getVocabularyRegistry().get( |
280 | 25 | spec, name='SpecificationDepCandidates') | 26 | spec, name='SpecificationDepCandidates') |
281 | 26 | 27 | ||
283 | 27 | def test_getTermByToken_product(self): | 28 | def test_getTermByToken_by_name_for_product(self): |
284 | 28 | # Calling getTermByToken for a dependency vocab for a spec from a | 29 | # Calling getTermByToken for a dependency vocab for a spec from a |
285 | 29 | # product with the name of an acceptable candidate spec returns the | 30 | # product with the name of an acceptable candidate spec returns the |
286 | 30 | # term for the candidate | 31 | # term for the candidate |
287 | @@ -35,7 +36,7 @@ | |||
288 | 35 | self.assertEqual( | 36 | self.assertEqual( |
289 | 36 | candidate, vocab.getTermByToken(candidate.name).value) | 37 | candidate, vocab.getTermByToken(candidate.name).value) |
290 | 37 | 38 | ||
292 | 38 | def test_getTermByToken_distro(self): | 39 | def test_getTermByToken_by_name_for_distro(self): |
293 | 39 | # Calling getTermByToken for a dependency vocab for a spec from a | 40 | # Calling getTermByToken for a dependency vocab for a spec from a |
294 | 40 | # distribution with the name of an acceptable candidate spec returns | 41 | # distribution with the name of an acceptable candidate spec returns |
295 | 41 | # the term for the candidate | 42 | # the term for the candidate |
296 | @@ -46,7 +47,55 @@ | |||
297 | 46 | self.assertEqual( | 47 | self.assertEqual( |
298 | 47 | candidate, vocab.getTermByToken(candidate.name).value) | 48 | candidate, vocab.getTermByToken(candidate.name).value) |
299 | 48 | 49 | ||
301 | 49 | def test_getTermByToken_disallows_blocked(self): | 50 | def test_getTermByToken_by_url_for_product(self): |
302 | 51 | # Calling getTermByToken with the full URL for a spec on a product | ||
303 | 52 | # returns that spec, irrespective of the context's target. | ||
304 | 53 | spec = self.factory.makeSpecification() | ||
305 | 54 | candidate = self.factory.makeSpecification( | ||
306 | 55 | product=self.factory.makeProduct()) | ||
307 | 56 | vocab = self.getVocabularyForSpec(spec) | ||
308 | 57 | self.assertEqual( | ||
309 | 58 | candidate, vocab.getTermByToken(canonical_url(candidate)).value) | ||
310 | 59 | |||
311 | 60 | def test_getTermByToken_by_url_for_distro(self): | ||
312 | 61 | # Calling getTermByToken with the full URL for a spec on a | ||
313 | 62 | # distribution returns that spec, irrespective of the context's | ||
314 | 63 | # target. | ||
315 | 64 | spec = self.factory.makeSpecification() | ||
316 | 65 | candidate = self.factory.makeSpecification( | ||
317 | 66 | distribution=self.factory.makeDistribution()) | ||
318 | 67 | vocab = self.getVocabularyForSpec(spec) | ||
319 | 68 | self.assertEqual( | ||
320 | 69 | candidate, vocab.getTermByToken(canonical_url(candidate)).value) | ||
321 | 70 | |||
322 | 71 | def test_getTermByToken_lookup_error_on_nonsense(self): | ||
323 | 72 | # getTermByToken with the a string that does not name a spec raises | ||
324 | 73 | # LookupError. | ||
325 | 74 | product = self.factory.makeProduct() | ||
326 | 75 | spec = self.factory.makeSpecification(product=product) | ||
327 | 76 | vocab = self.getVocabularyForSpec(spec) | ||
328 | 77 | self.assertRaises( | ||
329 | 78 | LookupError, vocab.getTermByToken, self.factory.getUniqueString()) | ||
330 | 79 | |||
331 | 80 | def test_getTermByToken_lookup_error_on_url_with_invalid_pillar(self): | ||
332 | 81 | # getTermByToken with the a string that looks like a blueprint URL but | ||
333 | 82 | # has an invalid pillar name raises LookupError. | ||
334 | 83 | spec = self.factory.makeSpecification() | ||
335 | 84 | url = canonical_url(spec).replace( | ||
336 | 85 | spec.target.name, self.factory.getUniqueString()) | ||
337 | 86 | vocab = self.getVocabularyForSpec(spec) | ||
338 | 87 | self.assertRaises(LookupError, vocab.getTermByToken, url) | ||
339 | 88 | |||
340 | 89 | def test_getTermByToken_lookup_error_on_url_with_invalid_spec_name(self): | ||
341 | 90 | # getTermByToken with the a string that looks like a blueprint URL but | ||
342 | 91 | # has an invalid spec name raises LookupError. | ||
343 | 92 | spec = self.factory.makeSpecification() | ||
344 | 93 | url = canonical_url(spec).replace( | ||
345 | 94 | spec.name, self.factory.getUniqueString()) | ||
346 | 95 | vocab = self.getVocabularyForSpec(spec) | ||
347 | 96 | self.assertRaises(LookupError, vocab.getTermByToken, url) | ||
348 | 97 | |||
349 | 98 | def test_getTermByToken_by_name_disallows_blocked(self): | ||
350 | 50 | # getTermByToken with the name of an candidate spec that is blocked by | 99 | # getTermByToken with the name of an candidate spec that is blocked by |
351 | 51 | # the vocab's context raises LookupError. | 100 | # the vocab's context raises LookupError. |
352 | 52 | product = self.factory.makeProduct() | 101 | product = self.factory.makeProduct() |
353 | @@ -56,17 +105,74 @@ | |||
354 | 56 | vocab = self.getVocabularyForSpec(spec) | 105 | vocab = self.getVocabularyForSpec(spec) |
355 | 57 | self.assertRaises(LookupError, vocab.getTermByToken, candidate.name) | 106 | self.assertRaises(LookupError, vocab.getTermByToken, candidate.name) |
356 | 58 | 107 | ||
358 | 59 | def test_getTermByToken_disallows_context(self): | 108 | def test_getTermByToken_by_url_disallows_blocked(self): |
359 | 109 | # getTermByToken with the URL of an candidate spec that is blocked by | ||
360 | 110 | # the vocab's context raises LookupError. | ||
361 | 111 | spec = self.factory.makeSpecification() | ||
362 | 112 | candidate = self.factory.makeSpecification() | ||
363 | 113 | candidate.createDependency(spec) | ||
364 | 114 | vocab = self.getVocabularyForSpec(spec) | ||
365 | 115 | self.assertRaises( | ||
366 | 116 | LookupError, vocab.getTermByToken, canonical_url(candidate)) | ||
367 | 117 | |||
368 | 118 | def test_getTermByToken_by_name_disallows_context(self): | ||
369 | 60 | # getTermByToken with the name of the vocab's context raises | 119 | # getTermByToken with the name of the vocab's context raises |
370 | 61 | # LookupError. | 120 | # LookupError. |
371 | 62 | spec = self.factory.makeSpecification() | 121 | spec = self.factory.makeSpecification() |
372 | 63 | vocab = self.getVocabularyForSpec(spec) | 122 | vocab = self.getVocabularyForSpec(spec) |
373 | 64 | self.assertRaises(LookupError, vocab.getTermByToken, spec.name) | 123 | self.assertRaises(LookupError, vocab.getTermByToken, spec.name) |
374 | 65 | 124 | ||
376 | 66 | def test_getTermByToken_disallows_spec_for_other_target(self): | 125 | def test_getTermByToken_by_url_disallows_context(self): |
377 | 126 | # getTermByToken with the URL of the vocab's context raises | ||
378 | 127 | # LookupError. | ||
379 | 128 | spec = self.factory.makeSpecification() | ||
380 | 129 | vocab = self.getVocabularyForSpec(spec) | ||
381 | 130 | self.assertRaises( | ||
382 | 131 | LookupError, vocab.getTermByToken, canonical_url(spec)) | ||
383 | 132 | |||
384 | 133 | def test_getTermByToken_by_name_disallows_spec_for_other_target(self): | ||
385 | 67 | # getTermByToken with the name of a spec with a different target | 134 | # getTermByToken with the name of a spec with a different target |
386 | 68 | # raises LookupError. | 135 | # raises LookupError. |
387 | 69 | spec = self.factory.makeSpecification() | 136 | spec = self.factory.makeSpecification() |
388 | 70 | candidate = self.factory.makeSpecification() | 137 | candidate = self.factory.makeSpecification() |
389 | 71 | vocab = self.getVocabularyForSpec(spec) | 138 | vocab = self.getVocabularyForSpec(spec) |
390 | 72 | self.assertRaises(LookupError, vocab.getTermByToken, candidate.name) | 139 | self.assertRaises(LookupError, vocab.getTermByToken, candidate.name) |
391 | 140 | |||
392 | 141 | def test_searchForTerms_by_url(self): | ||
393 | 142 | # Calling searchForTerms with the URL of a valid candidate spec | ||
394 | 143 | # returns just that spec. | ||
395 | 144 | spec = self.factory.makeSpecification() | ||
396 | 145 | candidate = self.factory.makeSpecification() | ||
397 | 146 | vocab = self.getVocabularyForSpec(spec) | ||
398 | 147 | results = vocab.searchForTerms(canonical_url(candidate)) | ||
399 | 148 | self.assertEqual(1, len(results)) | ||
400 | 149 | self.assertEqual(candidate, list(results)[0].value) | ||
401 | 150 | |||
402 | 151 | def test_searchForTerms_by_url_rejects_invalid(self): | ||
403 | 152 | # Calling searchForTerms with the URL of a invalid candidate spec | ||
404 | 153 | # returns an empty iterator. | ||
405 | 154 | spec = self.factory.makeSpecification() | ||
406 | 155 | candidate = self.factory.makeSpecification() | ||
407 | 156 | candidate.createDependency(spec) | ||
408 | 157 | vocab = self.getVocabularyForSpec(spec) | ||
409 | 158 | results = vocab.searchForTerms(canonical_url(candidate)) | ||
410 | 159 | self.assertEqual(0, len(results)) | ||
411 | 160 | |||
412 | 161 | def test_token_for_same_target_dep_is_name(self): | ||
413 | 162 | # The 'token' part of the term for a dependency candidate that has the | ||
414 | 163 | # same target is just the name of the candidate. | ||
415 | 164 | product = self.factory.makeProduct() | ||
416 | 165 | spec = self.factory.makeSpecification(product=product) | ||
417 | 166 | candidate = self.factory.makeSpecification(product=product) | ||
418 | 167 | vocab = self.getVocabularyForSpec(spec) | ||
419 | 168 | term = vocab.getTermByToken(candidate.name) | ||
420 | 169 | self.assertEqual(term.token, candidate.name) | ||
421 | 170 | |||
422 | 171 | def test_token_for_different_target_dep_is_url(self): | ||
423 | 172 | # The 'token' part of the term for a dependency candidate that has a | ||
424 | 173 | # different target is the canonical url of the candidate. | ||
425 | 174 | spec = self.factory.makeSpecification() | ||
426 | 175 | candidate = self.factory.makeSpecification() | ||
427 | 176 | vocab = self.getVocabularyForSpec(spec) | ||
428 | 177 | term = vocab.getTermByToken(canonical_url(candidate)) | ||
429 | 178 | self.assertEqual(term.token, canonical_url(candidate)) | ||
430 | 73 | 179 | ||
431 | === modified file 'lib/lp/testing/factory.py' | |||
432 | --- lib/lp/testing/factory.py 2010-08-26 22:44:30 +0000 | |||
433 | +++ lib/lp/testing/factory.py 2010-08-27 04:52:44 +0000 | |||
434 | @@ -1496,7 +1496,7 @@ | |||
435 | 1496 | return mail | 1496 | return mail |
436 | 1497 | 1497 | ||
437 | 1498 | def makeSpecification(self, product=None, title=None, distribution=None, | 1498 | def makeSpecification(self, product=None, title=None, distribution=None, |
439 | 1499 | name=None, summary=None, | 1499 | name=None, summary=None, owner=None, |
440 | 1500 | status=SpecificationDefinitionStatus.NEW): | 1500 | status=SpecificationDefinitionStatus.NEW): |
441 | 1501 | """Create and return a new, arbitrary Blueprint. | 1501 | """Create and return a new, arbitrary Blueprint. |
442 | 1502 | 1502 | ||
443 | @@ -1511,13 +1511,15 @@ | |||
444 | 1511 | summary = self.getUniqueString('summary') | 1511 | summary = self.getUniqueString('summary') |
445 | 1512 | if title is None: | 1512 | if title is None: |
446 | 1513 | title = self.getUniqueString('title') | 1513 | title = self.getUniqueString('title') |
447 | 1514 | if owner is None: | ||
448 | 1515 | owner = self.makePerson() | ||
449 | 1514 | return getUtility(ISpecificationSet).new( | 1516 | return getUtility(ISpecificationSet).new( |
450 | 1515 | name=name, | 1517 | name=name, |
451 | 1516 | title=title, | 1518 | title=title, |
452 | 1517 | specurl=None, | 1519 | specurl=None, |
453 | 1518 | summary=summary, | 1520 | summary=summary, |
454 | 1519 | definition_status=status, | 1521 | definition_status=status, |
456 | 1520 | owner=self.makePerson(), | 1522 | owner=owner, |
457 | 1521 | product=product, | 1523 | product=product, |
458 | 1522 | distribution=distribution) | 1524 | distribution=distribution) |
459 | 1523 | 1525 |
I love all the new tests :)