Merge lp:~cjwatson/launchpad/contents-race into lp:launchpad
- contents-race
- Merge into devel
Proposed by
Colin Watson
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Colin Watson | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 17341 | ||||
Proposed branch: | lp:~cjwatson/launchpad/contents-race | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
391 lines (+158/-33) 7 files modified
lib/lp/archivepublisher/scripts/generate_contents_files.py (+5/-17) lib/lp/archivepublisher/scripts/publish_ftpmaster.py (+43/-1) lib/lp/archivepublisher/tests/test_generate_contents_files.py (+11/-10) lib/lp/archivepublisher/tests/test_publish_ftpmaster.py (+72/-1) lib/lp/registry/interfaces/distribution.py (+4/-1) lib/lp/registry/model/distribution.py (+7/-1) lib/lp/registry/tests/test_distribution.py (+16/-2) |
||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/contents-race | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+249547@code.launchpad.net |
Commit message
Update Contents files in a way that doesn't suffer from races between generate-
Description of the change
Update Contents files in a way that doesn't suffer from races between generate-
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/archivepublisher/scripts/generate_contents_files.py' | |||
2 | --- lib/lp/archivepublisher/scripts/generate_contents_files.py 2012-11-10 02:25:07 +0000 | |||
3 | +++ lib/lp/archivepublisher/scripts/generate_contents_files.py 2015-02-13 10:45:35 +0000 | |||
4 | @@ -1,4 +1,4 @@ | |||
6 | 1 | # Copyright 2011-2012 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2011-2015 Canonical Ltd. This software is licensed under the |
7 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
8 | 3 | 3 | ||
9 | 4 | """Archive Contents files generator.""" | 4 | """Archive Contents files generator.""" |
10 | @@ -16,7 +16,6 @@ | |||
11 | 16 | from lp.archivepublisher.config import getPubConfig | 16 | from lp.archivepublisher.config import getPubConfig |
12 | 17 | from lp.registry.interfaces.distribution import IDistributionSet | 17 | from lp.registry.interfaces.distribution import IDistributionSet |
13 | 18 | from lp.registry.interfaces.pocket import PackagePublishingPocket | 18 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
14 | 19 | from lp.registry.interfaces.series import SeriesStatus | ||
15 | 20 | from lp.services.command_spawner import ( | 19 | from lp.services.command_spawner import ( |
16 | 21 | CommandSpawner, | 20 | CommandSpawner, |
17 | 22 | OutputLineHandler, | 21 | OutputLineHandler, |
18 | @@ -132,20 +131,9 @@ | |||
19 | 132 | if not file_exists(path): | 131 | if not file_exists(path): |
20 | 133 | os.makedirs(path) | 132 | os.makedirs(path) |
21 | 134 | 133 | ||
22 | 135 | def getSupportedSeries(self): | ||
23 | 136 | """Return suites that are supported in this distribution. | ||
24 | 137 | |||
25 | 138 | "Supported" means not EXPERIMENTAL or OBSOLETE. | ||
26 | 139 | """ | ||
27 | 140 | unsupported_status = (SeriesStatus.EXPERIMENTAL, | ||
28 | 141 | SeriesStatus.OBSOLETE) | ||
29 | 142 | for series in self.distribution: | ||
30 | 143 | if series.status not in unsupported_status: | ||
31 | 144 | yield series | ||
32 | 145 | |||
33 | 146 | def getSuites(self): | 134 | def getSuites(self): |
36 | 147 | """Return suites that are actually supported in this distribution.""" | 135 | """Return suites that need Contents files.""" |
37 | 148 | for series in self.getSupportedSeries(): | 136 | for series in self.distribution.getNonObsoleteSeries(): |
38 | 149 | for pocket in PackagePublishingPocket.items: | 137 | for pocket in PackagePublishingPocket.items: |
39 | 150 | suite = series.getSuite(pocket) | 138 | suite = series.getSuite(pocket) |
40 | 151 | if file_exists(os.path.join(self.config.distsroot, suite)): | 139 | if file_exists(os.path.join(self.config.distsroot, suite)): |
41 | @@ -269,12 +257,12 @@ | |||
42 | 269 | # re-fetch them unnecessarily. | 257 | # re-fetch them unnecessarily. |
43 | 270 | if differ_in_content(current_contents, last_contents): | 258 | if differ_in_content(current_contents, last_contents): |
44 | 271 | self.logger.debug( | 259 | self.logger.debug( |
46 | 272 | "Installing new Contents file for %s/%s.", suite, arch) | 260 | "Staging new Contents file for %s/%s.", suite, arch) |
47 | 273 | 261 | ||
48 | 274 | new_contents = os.path.join( | 262 | new_contents = os.path.join( |
49 | 275 | contents_dir, "%s.gz" % contents_filename) | 263 | contents_dir, "%s.gz" % contents_filename) |
50 | 276 | contents_dest = os.path.join( | 264 | contents_dest = os.path.join( |
52 | 277 | self.config.distsroot, suite, "%s.gz" % contents_filename) | 265 | contents_dir, "%s-staged.gz" % contents_filename) |
53 | 278 | 266 | ||
54 | 279 | os.rename(current_contents, last_contents) | 267 | os.rename(current_contents, last_contents) |
55 | 280 | os.rename(new_contents, contents_dest) | 268 | os.rename(new_contents, contents_dest) |
56 | 281 | 269 | ||
57 | === modified file 'lib/lp/archivepublisher/scripts/publish_ftpmaster.py' | |||
58 | --- lib/lp/archivepublisher/scripts/publish_ftpmaster.py 2014-08-09 19:45:00 +0000 | |||
59 | +++ lib/lp/archivepublisher/scripts/publish_ftpmaster.py 2015-02-13 10:45:35 +0000 | |||
60 | @@ -1,4 +1,4 @@ | |||
62 | 1 | # Copyright 2011-2012 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2011-2015 Canonical Ltd. This software is licensed under the |
63 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
64 | 3 | 3 | ||
65 | 4 | """Master distro publishing script.""" | 4 | """Master distro publishing script.""" |
66 | @@ -10,6 +10,7 @@ | |||
67 | 10 | 10 | ||
68 | 11 | from datetime import datetime | 11 | from datetime import datetime |
69 | 12 | import os | 12 | import os |
70 | 13 | import shutil | ||
71 | 13 | 14 | ||
72 | 14 | from pytz import utc | 15 | from pytz import utc |
73 | 15 | from zope.component import getUtility | 16 | from zope.component import getUtility |
74 | @@ -156,6 +157,19 @@ | |||
75 | 156 | for purpose, config in candidates if config is not None) | 157 | for purpose, config in candidates if config is not None) |
76 | 157 | 158 | ||
77 | 158 | 159 | ||
78 | 160 | def newer_mtime(one_file, other_file): | ||
79 | 161 | """Is one_file newer than other_file, or is other_file missing?""" | ||
80 | 162 | try: | ||
81 | 163 | one_mtime = os.stat(one_file).st_mtime | ||
82 | 164 | except OSError: | ||
83 | 165 | return False | ||
84 | 166 | try: | ||
85 | 167 | other_mtime = os.stat(other_file).st_mtime | ||
86 | 168 | except OSError: | ||
87 | 169 | return True | ||
88 | 170 | return one_mtime > other_mtime | ||
89 | 171 | |||
90 | 172 | |||
91 | 159 | class PublishFTPMaster(LaunchpadCronScript): | 173 | class PublishFTPMaster(LaunchpadCronScript): |
92 | 160 | """Publish a distro (update). | 174 | """Publish a distro (update). |
93 | 161 | 175 | ||
94 | @@ -539,6 +553,32 @@ | |||
95 | 539 | # most of its time. | 553 | # most of its time. |
96 | 540 | self.publishDistroArchive(distribution, archive) | 554 | self.publishDistroArchive(distribution, archive) |
97 | 541 | 555 | ||
98 | 556 | def updateContentsFile(self, distribution, suite, arch): | ||
99 | 557 | """Update a single Contents file if necessary.""" | ||
100 | 558 | config = self.configs[distribution][ArchivePurpose.PRIMARY] | ||
101 | 559 | backup_dists = get_backup_dists(config) | ||
102 | 560 | content_dists = os.path.join( | ||
103 | 561 | config.distroroot, "contents-generation", distribution.name, | ||
104 | 562 | "dists") | ||
105 | 563 | contents_filename = "Contents-%s" % arch.architecturetag | ||
106 | 564 | current_contents = os.path.join( | ||
107 | 565 | backup_dists, suite, "%s.gz" % contents_filename) | ||
108 | 566 | new_contents = os.path.join( | ||
109 | 567 | content_dists, suite, "%s-staged.gz" % contents_filename) | ||
110 | 568 | if newer_mtime(new_contents, current_contents): | ||
111 | 569 | self.logger.debug( | ||
112 | 570 | "Installing new Contents file for %s/%s.", suite, | ||
113 | 571 | arch.architecturetag) | ||
114 | 572 | shutil.copy2(new_contents, current_contents) | ||
115 | 573 | |||
116 | 574 | def updateContentsFiles(self, distribution): | ||
117 | 575 | """Pick up updated Contents files if necessary.""" | ||
118 | 576 | for series in distribution.getNonObsoleteSeries(): | ||
119 | 577 | for pocket in PackagePublishingPocket.items: | ||
120 | 578 | suite = series.getSuite(pocket) | ||
121 | 579 | for arch in series.enabled_architectures: | ||
122 | 580 | self.updateContentsFile(distribution, suite, arch) | ||
123 | 581 | |||
124 | 542 | def publish(self, distribution, security_only=False): | 582 | def publish(self, distribution, security_only=False): |
125 | 543 | """Do the main publishing work. | 583 | """Do the main publishing work. |
126 | 544 | 584 | ||
127 | @@ -558,6 +598,8 @@ | |||
128 | 558 | # Let's assume the main archive is always modified | 598 | # Let's assume the main archive is always modified |
129 | 559 | has_published = True | 599 | has_published = True |
130 | 560 | 600 | ||
131 | 601 | self.updateContentsFiles(distribution) | ||
132 | 602 | |||
133 | 561 | # Swizzle the now-updated backup dists and the current dists | 603 | # Swizzle the now-updated backup dists and the current dists |
134 | 562 | # around. | 604 | # around. |
135 | 563 | self.installDists(distribution) | 605 | self.installDists(distribution) |
136 | 564 | 606 | ||
137 | === modified file 'lib/lp/archivepublisher/tests/test_generate_contents_files.py' | |||
138 | --- lib/lp/archivepublisher/tests/test_generate_contents_files.py 2013-09-11 08:17:34 +0000 | |||
139 | +++ lib/lp/archivepublisher/tests/test_generate_contents_files.py 2015-02-13 10:45:35 +0000 | |||
140 | @@ -1,4 +1,4 @@ | |||
142 | 1 | # Copyright 2011-2012 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2011-2015 Canonical Ltd. This software is licensed under the |
143 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
144 | 3 | 3 | ||
145 | 4 | """Test for the `generate-contents-files` script.""" | 4 | """Test for the `generate-contents-files` script.""" |
146 | @@ -15,6 +15,7 @@ | |||
147 | 15 | execute, | 15 | execute, |
148 | 16 | GenerateContentsFiles, | 16 | GenerateContentsFiles, |
149 | 17 | ) | 17 | ) |
150 | 18 | from lp.archivepublisher.scripts.publish_ftpmaster import PublishFTPMaster | ||
151 | 18 | from lp.registry.interfaces.pocket import PackagePublishingPocket | 19 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
152 | 19 | from lp.services.log.logger import DevNullLogger | 20 | from lp.services.log.logger import DevNullLogger |
153 | 20 | from lp.services.osutils import write_file | 21 | from lp.services.osutils import write_file |
154 | @@ -183,14 +184,6 @@ | |||
155 | 183 | self.assertEqual( | 184 | self.assertEqual( |
156 | 184 | [das.architecturetag], script.getArchs(distroseries.name)) | 185 | [das.architecturetag], script.getArchs(distroseries.name)) |
157 | 185 | 186 | ||
158 | 186 | def test_getSupportedSeries(self): | ||
159 | 187 | # getSupportedSeries returns the supported distroseries in the | ||
160 | 188 | # distribution. | ||
161 | 189 | script = self.makeScript() | ||
162 | 190 | distroseries = self.factory.makeDistroSeries( | ||
163 | 191 | distribution=script.distribution) | ||
164 | 192 | self.assertIn(distroseries, script.getSupportedSeries()) | ||
165 | 193 | |||
166 | 194 | def test_getSuites(self): | 187 | def test_getSuites(self): |
167 | 195 | # getSuites returns the full names (distroseries-pocket) of the | 188 | # getSuites returns the full names (distroseries-pocket) of the |
168 | 196 | # pockets that have packages to publish. | 189 | # pockets that have packages to publish. |
169 | @@ -268,7 +261,8 @@ | |||
170 | 268 | script.content_archive, StartsWith(script.config.distroroot)) | 261 | script.content_archive, StartsWith(script.config.distroroot)) |
171 | 269 | 262 | ||
172 | 270 | def test_main(self): | 263 | def test_main(self): |
174 | 271 | # If run end-to-end, the script generates Contents.gz files. | 264 | # If run end-to-end, the script generates Contents.gz files, and a |
175 | 265 | # following publisher run will put those files in their final place. | ||
176 | 272 | distro = self.makeDistro() | 266 | distro = self.makeDistro() |
177 | 273 | distroseries = self.factory.makeDistroSeries(distribution=distro) | 267 | distroseries = self.factory.makeDistroSeries(distribution=distro) |
178 | 274 | processor = self.factory.makeProcessor() | 268 | processor = self.factory.makeProcessor() |
179 | @@ -287,6 +281,13 @@ | |||
180 | 287 | fake_overrides(script, distroseries) | 281 | fake_overrides(script, distroseries) |
181 | 288 | script.process() | 282 | script.process() |
182 | 289 | self.assertTrue(file_exists(os.path.join( | 283 | self.assertTrue(file_exists(os.path.join( |
183 | 284 | script.content_archive, distro.name, "dists", suite, | ||
184 | 285 | "Contents-%s-staged.gz" % das.architecturetag))) | ||
185 | 286 | publisher_script = PublishFTPMaster(test_args=["-d", distro.name]) | ||
186 | 287 | publisher_script.txn = self.layer.txn | ||
187 | 288 | publisher_script.logger = DevNullLogger() | ||
188 | 289 | publisher_script.main() | ||
189 | 290 | self.assertTrue(file_exists(os.path.join( | ||
190 | 290 | script.config.distsroot, suite, | 291 | script.config.distsroot, suite, |
191 | 291 | "Contents-%s.gz" % das.architecturetag))) | 292 | "Contents-%s.gz" % das.architecturetag))) |
192 | 292 | 293 | ||
193 | 293 | 294 | ||
194 | === modified file 'lib/lp/archivepublisher/tests/test_publish_ftpmaster.py' | |||
195 | --- lib/lp/archivepublisher/tests/test_publish_ftpmaster.py 2014-04-15 16:32:03 +0000 | |||
196 | +++ lib/lp/archivepublisher/tests/test_publish_ftpmaster.py 2015-02-13 10:45:35 +0000 | |||
197 | @@ -1,4 +1,4 @@ | |||
199 | 1 | # Copyright 2011-2012 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2011-2015 Canonical Ltd. This software is licensed under the |
200 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
201 | 3 | 3 | ||
202 | 4 | """Test publish-ftpmaster cron script.""" | 4 | """Test publish-ftpmaster cron script.""" |
203 | @@ -8,6 +8,7 @@ | |||
204 | 8 | import logging | 8 | import logging |
205 | 9 | import os | 9 | import os |
206 | 10 | from textwrap import dedent | 10 | from textwrap import dedent |
207 | 11 | import time | ||
208 | 11 | 12 | ||
209 | 12 | from apt_pkg import TagFile | 13 | from apt_pkg import TagFile |
210 | 13 | from testtools.matchers import ( | 14 | from testtools.matchers import ( |
211 | @@ -24,6 +25,7 @@ | |||
212 | 24 | compose_shell_boolean, | 25 | compose_shell_boolean, |
213 | 25 | find_run_parts_dir, | 26 | find_run_parts_dir, |
214 | 26 | get_working_dists, | 27 | get_working_dists, |
215 | 28 | newer_mtime, | ||
216 | 27 | PublishFTPMaster, | 29 | PublishFTPMaster, |
217 | 28 | shell_quote, | 30 | shell_quote, |
218 | 29 | ) | 31 | ) |
219 | @@ -38,6 +40,7 @@ | |||
220 | 38 | BufferLogger, | 40 | BufferLogger, |
221 | 39 | DevNullLogger, | 41 | DevNullLogger, |
222 | 40 | ) | 42 | ) |
223 | 43 | from lp.services.osutils import write_file | ||
224 | 41 | from lp.services.scripts.base import LaunchpadScriptFailure | 44 | from lp.services.scripts.base import LaunchpadScriptFailure |
225 | 42 | from lp.services.utils import file_exists | 45 | from lp.services.utils import file_exists |
226 | 43 | from lp.soyuz.enums import ( | 46 | from lp.soyuz.enums import ( |
227 | @@ -208,6 +211,46 @@ | |||
228 | 208 | self.assertEqual("no", compose_shell_boolean(False)) | 211 | self.assertEqual("no", compose_shell_boolean(False)) |
229 | 209 | 212 | ||
230 | 210 | 213 | ||
231 | 214 | class TestNewerMtime(TestCase): | ||
232 | 215 | |||
233 | 216 | def setUp(self): | ||
234 | 217 | super(TestCase, self).setUp() | ||
235 | 218 | tempdir = self.useTempDir() | ||
236 | 219 | self.a = os.path.join(tempdir, "a") | ||
237 | 220 | self.b = os.path.join(tempdir, "b") | ||
238 | 221 | |||
239 | 222 | def test_both_missing(self): | ||
240 | 223 | self.assertFalse(newer_mtime(self.a, self.b)) | ||
241 | 224 | |||
242 | 225 | def test_one_missing(self): | ||
243 | 226 | write_file(self.b, "") | ||
244 | 227 | self.assertFalse(newer_mtime(self.a, self.b)) | ||
245 | 228 | |||
246 | 229 | def test_other_missing(self): | ||
247 | 230 | write_file(self.a, "") | ||
248 | 231 | self.assertTrue(newer_mtime(self.a, self.b)) | ||
249 | 232 | |||
250 | 233 | def test_older(self): | ||
251 | 234 | write_file(self.a, "") | ||
252 | 235 | os.utime(self.a, (0, 0)) | ||
253 | 236 | write_file(self.b, "") | ||
254 | 237 | self.assertFalse(newer_mtime(self.a, self.b)) | ||
255 | 238 | |||
256 | 239 | def test_equal(self): | ||
257 | 240 | now = time.time() | ||
258 | 241 | write_file(self.a, "") | ||
259 | 242 | os.utime(self.a, (now, now)) | ||
260 | 243 | write_file(self.b, "") | ||
261 | 244 | os.utime(self.b, (now, now)) | ||
262 | 245 | self.assertFalse(newer_mtime(self.a, self.b)) | ||
263 | 246 | |||
264 | 247 | def test_newer(self): | ||
265 | 248 | write_file(self.a, "") | ||
266 | 249 | write_file(self.b, "") | ||
267 | 250 | os.utime(self.b, (0, 0)) | ||
268 | 251 | self.assertTrue(newer_mtime(self.a, self.b)) | ||
269 | 252 | |||
270 | 253 | |||
271 | 211 | class TestFindRunPartsDir(TestCaseWithFactory, HelpersMixin): | 254 | class TestFindRunPartsDir(TestCaseWithFactory, HelpersMixin): |
272 | 212 | layer = ZopelessDatabaseLayer | 255 | layer = ZopelessDatabaseLayer |
273 | 213 | 256 | ||
274 | @@ -796,6 +839,34 @@ | |||
275 | 796 | "Did not find expected marker for %s." | 839 | "Did not find expected marker for %s." |
276 | 797 | % archive.purpose.title) | 840 | % archive.purpose.title) |
277 | 798 | 841 | ||
278 | 842 | def test_updateContentsFile_installs_changed(self): | ||
279 | 843 | distro = self.makeDistroWithPublishDirectory() | ||
280 | 844 | distroseries = self.factory.makeDistroSeries(distribution=distro) | ||
281 | 845 | das = self.factory.makeDistroArchSeries(distroseries=distroseries) | ||
282 | 846 | script = self.makeScript(distro) | ||
283 | 847 | script.setUp() | ||
284 | 848 | script.setUpDirs() | ||
285 | 849 | archive_config = getPubConfig(distro.main_archive) | ||
286 | 850 | contents_filename = "Contents-%s" % das.architecturetag | ||
287 | 851 | backup_suite = os.path.join( | ||
288 | 852 | archive_config.archiveroot + "-distscopy", "dists", | ||
289 | 853 | distroseries.name) | ||
290 | 854 | os.makedirs(backup_suite) | ||
291 | 855 | write_marker_file( | ||
292 | 856 | [backup_suite, "%s.gz" % contents_filename], "Old Contents") | ||
293 | 857 | os.utime( | ||
294 | 858 | os.path.join(backup_suite, "%s.gz" % contents_filename), (0, 0)) | ||
295 | 859 | content_suite = os.path.join( | ||
296 | 860 | archive_config.distroroot, "contents-generation", distro.name, | ||
297 | 861 | "dists", distroseries.name) | ||
298 | 862 | os.makedirs(content_suite) | ||
299 | 863 | write_marker_file( | ||
300 | 864 | [content_suite, "%s-staged.gz" % contents_filename], "Contents") | ||
301 | 865 | script.updateContentsFile(distro, distroseries.name, das) | ||
302 | 866 | self.assertEqual( | ||
303 | 867 | "Contents", | ||
304 | 868 | read_marker_file([backup_suite, "%s.gz" % contents_filename])) | ||
305 | 869 | |||
306 | 799 | def test_publish_always_returns_true_for_primary(self): | 870 | def test_publish_always_returns_true_for_primary(self): |
307 | 800 | script = self.makeScript() | 871 | script = self.makeScript() |
308 | 801 | script.publishDistroUploads = FakeMethod() | 872 | script.publishDistroUploads = FakeMethod() |
309 | 802 | 873 | ||
310 | === modified file 'lib/lp/registry/interfaces/distribution.py' | |||
311 | --- lib/lp/registry/interfaces/distribution.py 2014-11-17 18:36:16 +0000 | |||
312 | +++ lib/lp/registry/interfaces/distribution.py 2015-02-13 10:45:35 +0000 | |||
313 | @@ -1,4 +1,4 @@ | |||
315 | 1 | # Copyright 2009-2013 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
316 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
317 | 3 | 3 | ||
318 | 4 | """Interfaces including and related to IDistribution.""" | 4 | """Interfaces including and related to IDistribution.""" |
319 | @@ -408,6 +408,9 @@ | |||
320 | 408 | def getDevelopmentSeries(): | 408 | def getDevelopmentSeries(): |
321 | 409 | """Return the DistroSeries which are marked as in development.""" | 409 | """Return the DistroSeries which are marked as in development.""" |
322 | 410 | 410 | ||
323 | 411 | def getNonObsoleteSeries(): | ||
324 | 412 | """Return the non-OBSOLETE DistroSeries in this distribution.""" | ||
325 | 413 | |||
326 | 411 | def resolveSeriesAlias(name): | 414 | def resolveSeriesAlias(name): |
327 | 412 | """Resolve a series alias. | 415 | """Resolve a series alias. |
328 | 413 | 416 | ||
329 | 414 | 417 | ||
330 | === modified file 'lib/lp/registry/model/distribution.py' | |||
331 | --- lib/lp/registry/model/distribution.py 2014-12-08 04:20:17 +0000 | |||
332 | +++ lib/lp/registry/model/distribution.py 2015-02-13 10:45:35 +0000 | |||
333 | @@ -1,4 +1,4 @@ | |||
335 | 1 | # Copyright 2009-2013 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
336 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
337 | 3 | 3 | ||
338 | 4 | """Database classes for implementing distribution items.""" | 4 | """Database classes for implementing distribution items.""" |
339 | @@ -785,6 +785,12 @@ | |||
340 | 785 | distribution=self, | 785 | distribution=self, |
341 | 786 | status=SeriesStatus.DEVELOPMENT) | 786 | status=SeriesStatus.DEVELOPMENT) |
342 | 787 | 787 | ||
343 | 788 | def getNonObsoleteSeries(self): | ||
344 | 789 | """See `IDistribution`.""" | ||
345 | 790 | for series in self.series: | ||
346 | 791 | if series.status != SeriesStatus.OBSOLETE: | ||
347 | 792 | yield series | ||
348 | 793 | |||
349 | 788 | def getMilestone(self, name): | 794 | def getMilestone(self, name): |
350 | 789 | """See `IDistribution`.""" | 795 | """See `IDistribution`.""" |
351 | 790 | return Milestone.selectOne(""" | 796 | return Milestone.selectOne(""" |
352 | 791 | 797 | ||
353 | === modified file 'lib/lp/registry/tests/test_distribution.py' | |||
354 | --- lib/lp/registry/tests/test_distribution.py 2013-08-01 14:09:45 +0000 | |||
355 | +++ lib/lp/registry/tests/test_distribution.py 2015-02-13 10:45:35 +0000 | |||
356 | @@ -1,4 +1,4 @@ | |||
358 | 1 | # Copyright 2009-2013 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
359 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
360 | 3 | 3 | ||
361 | 4 | """Tests for Distribution.""" | 4 | """Tests for Distribution.""" |
362 | @@ -383,7 +383,7 @@ | |||
363 | 383 | 383 | ||
364 | 384 | 384 | ||
365 | 385 | class SeriesTests(TestCaseWithFactory): | 385 | class SeriesTests(TestCaseWithFactory): |
367 | 386 | """Test IDistribution.getSeries(). | 386 | """Test IDistribution.getSeries() and friends. |
368 | 387 | """ | 387 | """ |
369 | 388 | 388 | ||
370 | 389 | layer = LaunchpadFunctionalLayer | 389 | layer = LaunchpadFunctionalLayer |
371 | @@ -416,6 +416,20 @@ | |||
372 | 416 | self.assertEqual( | 416 | self.assertEqual( |
373 | 417 | series, distro.getSeries("devel", follow_aliases=True)) | 417 | series, distro.getSeries("devel", follow_aliases=True)) |
374 | 418 | 418 | ||
375 | 419 | def test_getNonObsoleteSeries(self): | ||
376 | 420 | distro = self.factory.makeDistribution() | ||
377 | 421 | self.factory.makeDistroSeries( | ||
378 | 422 | distribution=distro, status=SeriesStatus.OBSOLETE) | ||
379 | 423 | current = self.factory.makeDistroSeries( | ||
380 | 424 | distribution=distro, status=SeriesStatus.CURRENT) | ||
381 | 425 | development = self.factory.makeDistroSeries( | ||
382 | 426 | distribution=distro, status=SeriesStatus.DEVELOPMENT) | ||
383 | 427 | experimental = self.factory.makeDistroSeries( | ||
384 | 428 | distribution=distro, status=SeriesStatus.EXPERIMENTAL) | ||
385 | 429 | self.assertContentEqual( | ||
386 | 430 | [current, development, experimental], | ||
387 | 431 | list(distro.getNonObsoleteSeries())) | ||
388 | 432 | |||
389 | 419 | 433 | ||
390 | 420 | class DerivativesTests(TestCaseWithFactory): | 434 | class DerivativesTests(TestCaseWithFactory): |
391 | 421 | """Test IDistribution.derivatives. | 435 | """Test IDistribution.derivatives. |
getSupportedSeries is probably not an ideal name, given that SeriesStatus. SUPPORTED exists.
Also, we might want to make the method itself less nonsensical. Experimental is an active series status, so it probably wants Contents files.