Merge lp:~michael.nelson/launchpad/510331-syncsources-latest-pub into lp:launchpad

Proposed by Michael Nelson
Status: Merged
Approved by: Michael Nelson
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~michael.nelson/launchpad/510331-syncsources-latest-pub
Merge into: lp:launchpad
Diff against target: 190 lines (+90/-22)
5 files modified
lib/lp/soyuz/doc/archive.txt (+11/-7)
lib/lp/soyuz/interfaces/archive.py (+2/-2)
lib/lp/soyuz/model/archive.py (+24/-11)
lib/lp/soyuz/stories/webservice/xx-archive.txt (+9/-2)
lib/lp/soyuz/tests/test_archive.py (+44/-0)
To merge this branch: bzr merge lp:~michael.nelson/launchpad/510331-syncsources-latest-pub
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+18070@code.launchpad.net

Commit message

Ensures that syncSources only chooses the latest *PUBLISHED* sources, rather than simply the latest sources.

To post a comment you must log in.
Revision history for this message
Michael Nelson (michael.nelson) wrote :

= Summary =
Fixes 510331 by ensuring that syncSources only finds pub records that are currently PUBLISHED.

== Proposed fix ==

== Pre-implementation notes ==

See note on the bug, as well as:
<noodles> ok, sounds good. Also with bug 510331
<bigjools> noodles: yeah that one's easy
<noodles> IArchive.syncSource() already allows you to specify the version... but with the solution you've suggested for syncSources()
<bigjools> ?
<noodles> it would mean that we'd not copy deleted sources even when there is no published one... is that expected?
<bigjools> yes, I think so
<noodles> OK. easy then.
<bigjools> well, would you expect it to?
<noodles> I'm imagining using the web ui, looking at a bunch of deleted sources and selecting them...
<bigjools> that's syncSource behaviour really
<bigjools> syncSources is more automatic
<bigjools> but we can make it explicit in the docstring
<noodles> OK.

== Implementation details ==

I wasn't sure of the best way to test the private helper method. I ended up going with removeSecurityProxy() for the test, but would be happy to hear about a neater solution?

== Tests ==
bin/test -vvt doc/archive.txt -t TestCollectLatestPublishedSources

== Demo and Q/A ==

We can Q/A this using the API on dogfood (or edge).

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/lp/soyuz/doc/archive.txt
  lib/lp/soyuz/tests/test_archive.py
  lib/lp/soyuz/model/archive.py

== Pylint notices ==

lib/lp/soyuz/model/archive.py
    1099: [C0301] Line too long (81/78) -- fixed.
    14: [F0401] Unable to import 'lazr.lifecycle.event' (No module named lifecycle)

Revision history for this message
Graham Binns (gmb) wrote :

Hi Michael,

This looks good to me. One minor nitpick, but otherwise it's fine to
land as-is.

> === modified file 'lib/lp/soyuz/model/archive.py'
> --- lib/lp/soyuz/model/archive.py 2010-01-12 08:31:42 +0000
> +++ lib/lp/soyuz/model/archive.py 2010-01-26 11:54:18 +0000
> @@ -1121,6 +1112,23 @@
>
> self._copySources([source], to_pocket, to_series, include_binaries)
>
> + def _collectLatestPublishedSources(self, from_archive, source_names):
> + """Private helper to collect the latest published sources for an
> + archive.
> + """
> + sources = []
> + for name in source_names:
> + # Check to see if the source package exists, and raise a useful
> + # error if it doesn't.

What qualifies as "A useful error" here? It'd be nice to say in the
comment - or better still in the docstring - "will raise a FooBarError
if the source package doesn't exist."

> + getUtility(ISourcePackageNameSet)[name]
> + # Grabbing the item at index 0 ensures it's the most recent
> + # publication.
> + sources.append(
> + from_archive.getPublishedSources(
> + name=name, exact_match=True,
> + status=PackagePublishingStatus.PUBLISHED)[0])
> + return sources
> +
> def _copySources(self, sources, to_pocket, to_series=None,
> include_binaries=False):
> """Private helper function to copy sources to this archive.
>

review: Approve (code)
Revision history for this message
Michael Nelson (michael.nelson) wrote :

<noodles775> gmb: great. Did you have any thoughts about a better way to test the private method (as mentioned in the MP)?
<gmb> noodles775: Argh, must've skipped that bit of the mp. Sorry. Hang on...
<gmb> noodles775: So, no, I can't think of a better way (which is why I never balked at the use of rSP()). The only way I could think of is to test it indirectly, but I'd rather have the direct test myself.
<noodles775> Great, thanks for that.

Revision history for this message
Michael Nelson (michael.nelson) wrote :

Hi gmb,

I just got back to this branch, tested it, and had errors or the webservice branch that tested syncSource(s) through the API. The error was simply because the sources used in the test weren't in the published status, but it was 500ing, rather than a nice API error, so I've updated the code to avoid that also.

Here's the diff:

=== modified file 'lib/lp/soyuz/model/archive.py'
--- lib/lp/soyuz/model/archive.py 2010-01-28 16:54:43 +0000
+++ lib/lp/soyuz/model/archive.py 2010-02-02 11:28:11 +0000
@@ -1127,10 +1127,11 @@
             getUtility(ISourcePackageNameSet)[name]
             # Grabbing the item at index 0 ensures it's the most recent
             # publication.
- sources.append(
- from_archive.getPublishedSources(
- name=name, exact_match=True,
- status=PackagePublishingStatus.PUBLISHED)[0])
+ published_sources = from_archive.getPublishedSources(
+ name=name, exact_match=True,
+ status=PackagePublishingStatus.PUBLISHED)
+ if published_sources.count() > 0:
+ sources.append(published_sources[0])
         return sources

     def _copySources(self, sources, to_pocket, to_series=None,

=== modified file 'lib/lp/soyuz/stories/webservice/xx-archive.txt'
--- lib/lp/soyuz/stories/webservice/xx-archive.txt 2009-10-07 08:28:03 +0000
+++ lib/lp/soyuz/stories/webservice/xx-archive.txt 2010-02-02 11:57:30 +0000
@@ -576,9 +576,12 @@
     ... sourcename="package1", version="1.0",
     ... archive=ubuntu_db.main_archive)

+ >>> from lp.soyuz.interfaces.publishing import (
+ ... PackagePublishingStatus)
     >>> ignore = test_publisher.getPubSource(
     ... sourcename="package1", version="1.1",
- ... archive=ubuntu_db.main_archive)
+ ... archive=ubuntu_db.main_archive,
+ ... status=PackagePublishingStatus.PUBLISHED)

     >>> ignore = test_publisher.getPubSource(
     ... sourcename="package2", version="1.0",
@@ -742,6 +745,8 @@

     >>> private_publication = test_publisher.createSource(
     ... cprov.archive, 'foocomm', '1.0-1')
+ >>> private_publication.secure_record.status = (
+ ... PackagePublishingStatus.PUBLISHED)

     >>> logout()

@@ -908,8 +913,10 @@
 version.

     >>> login('<email address hidden>')
- >>> unused = test_publisher.createSource(
+ >>> subsequent_version = test_publisher.createSource(
     ... cprov.archive, 'foocomm', '1.0-2')
+ >>> subsequent_version.secure_record.status = (
+ ... PackagePublishingStatus.PUBLISHED)
     >>> logout()

     >>> print cprov_webservice.named_post(

Revision history for this message
Graham Binns (gmb) wrote :

As discussed on IRC, you should test for `not foo.is_empty()` rather than `foo.count() > 0` as it's more efficient. Other than that, this change looks fine.

review: Approve (code)
Revision history for this message
Michael Nelson (michael.nelson) wrote :

<noodles775> gmb, jtv: http://pastebin.ubuntu.com/367524/
<noodles775> Looks like is_empty isn't included on the ISQLObjectResultSet in storm.zope.interfaces :/
<gmb> ?!
<gmb> Oh, ah.
<gmb> I see.
<gmb> That's suboptimal.
<gmb> noodles775: In that case, just go back to using .count(). You could maybe file a tech-debt bug about changing getPublishedSources() to use Storm, but I don't know how worthwhile that would be.
<noodles775> Yep, thanks.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/soyuz/doc/archive.txt'
2--- lib/lp/soyuz/doc/archive.txt 2010-01-12 08:59:23 +0000
3+++ lib/lp/soyuz/doc/archive.txt 2010-02-02 12:06:20 +0000
4@@ -2349,13 +2349,17 @@
5 >>> test_publisher.addFakeChroots(hoary)
6 >>> unused = test_publisher.setUpDefaultDistroSeries(hoary)
7 >>> discard = test_publisher.getPubSource(
8- ... sourcename="package1", version="1.0", archive=cprov.archive)
9- >>> discard = test_publisher.getPubSource(
10- ... sourcename="package1", version="1.1", archive=cprov.archive)
11- >>> discard = test_publisher.getPubSource(
12- ... sourcename="package2", version="1.0", archive=cprov.archive)
13- >>> discard = test_publisher.getPubSource(
14- ... sourcename="pack", version="1.0", archive=cprov.archive)
15+ ... sourcename="package1", version="1.0", archive=cprov.archive,
16+ ... status=PackagePublishingStatus.PUBLISHED)
17+ >>> discard = test_publisher.getPubSource(
18+ ... sourcename="package1", version="1.1", archive=cprov.archive,
19+ ... status=PackagePublishingStatus.PUBLISHED)
20+ >>> discard = test_publisher.getPubSource(
21+ ... sourcename="package2", version="1.0", archive=cprov.archive,
22+ ... status=PackagePublishingStatus.PUBLISHED)
23+ >>> discard = test_publisher.getPubSource(
24+ ... sourcename="pack", version="1.0", archive=cprov.archive,
25+ ... status=PackagePublishingStatus.PUBLISHED)
26
27 Now we have package1 1.0 and 1.1, and package2 1.0 in cprov's PPA. We
28 can ask syncSources to synchronise these packages into mark's PPA in the
29
30=== modified file 'lib/lp/soyuz/interfaces/archive.py'
31--- lib/lp/soyuz/interfaces/archive.py 2010-01-12 03:45:02 +0000
32+++ lib/lp/soyuz/interfaces/archive.py 2010-02-02 12:06:20 +0000
33@@ -896,8 +896,8 @@
34 to_series=None, include_binaries=False):
35 """Synchronise (copy) named sources into this archive from another.
36
37- It will copy the most recent versions of the named sources to
38- the destination archive if necessary.
39+ It will copy the most recent PUBLISHED versions of the named
40+ sources to the destination archive if necessary.
41
42 This operation will only succeeds when all requested packages
43 are synchronised between the archives. If any of the requested
44
45=== modified file 'lib/lp/soyuz/model/archive.py'
46--- lib/lp/soyuz/model/archive.py 2010-01-12 08:31:42 +0000
47+++ lib/lp/soyuz/model/archive.py 2010-02-02 12:06:20 +0000
48@@ -1096,17 +1096,8 @@
49 to_series=None, include_binaries=False):
50 """See `IArchive`."""
51 # Find and validate the source package names in source_names.
52- sources = []
53- for name in source_names:
54- # Check to see if the source package exists, and raise a useful
55- # error if it doesn't.
56- getUtility(ISourcePackageNameSet)[name]
57- # Grabbing the item at index 0 ensures it's the most recent
58- # publication.
59- sources.append(
60- from_archive.getPublishedSources(
61- name=name, exact_match=True)[0])
62-
63+ sources = self._collectLatestPublishedSources(
64+ from_archive, source_names)
65 self._copySources(sources, to_pocket, to_series, include_binaries)
66
67 def syncSource(self, source_name, version, from_archive, to_pocket,
68@@ -1121,6 +1112,28 @@
69
70 self._copySources([source], to_pocket, to_series, include_binaries)
71
72+ def _collectLatestPublishedSources(self, from_archive, source_names):
73+ """Private helper to collect the latest published sources for an
74+ archive.
75+
76+ :raises NoSuchSourcePackageName: If any of the source_names do not
77+ exist.
78+ """
79+ sources = []
80+ for name in source_names:
81+ # Check to see if the source package exists. This will raise
82+ # a NoSuchSourcePackageName exception if the source package
83+ # doesn't exist.
84+ getUtility(ISourcePackageNameSet)[name]
85+ # Grabbing the item at index 0 ensures it's the most recent
86+ # publication.
87+ published_sources = from_archive.getPublishedSources(
88+ name=name, exact_match=True,
89+ status=PackagePublishingStatus.PUBLISHED)
90+ if published_sources.count() > 0:
91+ sources.append(published_sources[0])
92+ return sources
93+
94 def _copySources(self, sources, to_pocket, to_series=None,
95 include_binaries=False):
96 """Private helper function to copy sources to this archive.
97
98=== modified file 'lib/lp/soyuz/stories/webservice/xx-archive.txt'
99--- lib/lp/soyuz/stories/webservice/xx-archive.txt 2009-10-07 08:28:03 +0000
100+++ lib/lp/soyuz/stories/webservice/xx-archive.txt 2010-02-02 12:06:20 +0000
101@@ -576,9 +576,12 @@
102 ... sourcename="package1", version="1.0",
103 ... archive=ubuntu_db.main_archive)
104
105+ >>> from lp.soyuz.interfaces.publishing import (
106+ ... PackagePublishingStatus)
107 >>> ignore = test_publisher.getPubSource(
108 ... sourcename="package1", version="1.1",
109- ... archive=ubuntu_db.main_archive)
110+ ... archive=ubuntu_db.main_archive,
111+ ... status=PackagePublishingStatus.PUBLISHED)
112
113 >>> ignore = test_publisher.getPubSource(
114 ... sourcename="package2", version="1.0",
115@@ -742,6 +745,8 @@
116
117 >>> private_publication = test_publisher.createSource(
118 ... cprov.archive, 'foocomm', '1.0-1')
119+ >>> private_publication.secure_record.status = (
120+ ... PackagePublishingStatus.PUBLISHED)
121
122 >>> logout()
123
124@@ -908,8 +913,10 @@
125 version.
126
127 >>> login('foo.bar@canonical.com')
128- >>> unused = test_publisher.createSource(
129+ >>> subsequent_version = test_publisher.createSource(
130 ... cprov.archive, 'foocomm', '1.0-2')
131+ >>> subsequent_version.secure_record.status = (
132+ ... PackagePublishingStatus.PUBLISHED)
133 >>> logout()
134
135 >>> print cprov_webservice.named_post(
136
137=== modified file 'lib/lp/soyuz/tests/test_archive.py'
138--- lib/lp/soyuz/tests/test_archive.py 2010-01-12 04:21:42 +0000
139+++ lib/lp/soyuz/tests/test_archive.py 2010-02-02 12:06:20 +0000
140@@ -513,6 +513,50 @@
141 self.archive.disable()
142 self.assertRaises(AssertionError, self.archive.disable)
143
144+class TestCollectLatestPublishedSources(TestCaseWithFactory):
145+ """Ensure that the private helper method works as expected."""
146+
147+ layer = LaunchpadZopelessLayer
148+
149+ def setUp(self):
150+ """Setup an archive with relevant publications."""
151+ super(TestCollectLatestPublishedSources, self).setUp()
152+ self.publisher = SoyuzTestPublisher()
153+ self.publisher.prepareBreezyAutotest()
154+
155+ # Create an archive with some published sources. We'll store
156+ # a reference to the naked archive so that we can call
157+ # the private method which is not defined on the interface.
158+ self.archive = self.factory.makeArchive()
159+ self.naked_archive = removeSecurityProxy(self.archive)
160+
161+ self.pub_1 = self.publisher.getPubSource(
162+ version='0.5.11~ppa1', archive=self.archive, sourcename="foo",
163+ status=PackagePublishingStatus.PUBLISHED)
164+
165+ self.pub_2 = self.publisher.getPubSource(
166+ version='0.5.11~ppa2', archive=self.archive, sourcename="foo",
167+ status=PackagePublishingStatus.PUBLISHED)
168+
169+ self.pub_3 = self.publisher.getPubSource(
170+ version='0.9', archive=self.archive, sourcename="bar",
171+ status=PackagePublishingStatus.PUBLISHED)
172+
173+ def test_collectLatestPublishedSources_returns_latest(self):
174+ pubs = self.naked_archive._collectLatestPublishedSources(
175+ self.archive, ["foo"])
176+ self.assertEqual(1, len(pubs))
177+ self.assertEqual('0.5.11~ppa2', pubs[0].source_package_version)
178+
179+ def test_collectLatestPublishedSources_returns_published_only(self):
180+ # Set the status of the latest pub to DELETED and ensure that it
181+ # is not returned.
182+ self.pub_2.secure_record.status = PackagePublishingStatus.DELETED
183+
184+ pubs = self.naked_archive._collectLatestPublishedSources(
185+ self.archive, ["foo"])
186+ self.assertEqual(1, len(pubs))
187+ self.assertEqual('0.5.11~ppa1', pubs[0].source_package_version)
188
189 def test_suite():
190 return unittest.TestLoader().loadTestsFromName(__name__)