Merge lp:~jcsackett/launchpad/shortlist-696913 into lp:launchpad

Proposed by j.c.sackett
Status: Merged
Approved by: j.c.sackett
Approved revision: no longer in the source branch.
Merged at revision: 12194
Proposed branch: lp:~jcsackett/launchpad/shortlist-696913
Merge into: lp:launchpad
Diff against target: 359 lines (+149/-30)
7 files modified
lib/lp/blueprints/interfaces/specificationtarget.py (+5/-4)
lib/lp/blueprints/tests/test_hasspecifications.py (+38/-1)
lib/lp/registry/interfaces/milestone.py (+5/-5)
lib/lp/registry/interfaces/productseries.py (+5/-4)
lib/lp/registry/tests/test_milestone.py (+38/-10)
lib/lp/registry/tests/test_productseries.py (+20/-5)
lib/lp/testing/matchers.py (+38/-1)
To merge this branch: bzr merge lp:~jcsackett/launchpad/shortlist-696913
Reviewer Review Type Date Requested Status
Edwin Grubbs (community) code Approve
Benji York (community) code* Approve
Jeroen T. Vermeulen (community) Needs Fixing
Review via email: mp+45938@code.launchpad.net

Commit message

[r=benji,edwin-grubbs][ui=none][bug=696913] Adds doNotSnapshot to fields involved in OOPSes with ShortlistTooBigErrors.

Description of the change

Summary
=======

Because we are currently snapshotting most fields in pillars when snapshot is called for. However, certain collections don't need to be snapshotted, and cause an error because the number of objects in the collection exceed hardcoded limits.

This branch adds doNotSnapshot to those collections.

Proposed Fix
============

Add doNotSnapshot to the offending fields. A better solution would involve altering Snapshot to ignore (or be able to ignore) CollectionFields, but as this bug is holding up other work, the simple fix should be landed first. The better solution has been filed as bug 701715.

Preimplementation Talk
======================

Spoke with Curtis Hovey about the problem and the interactions of Snapshot and the various fields. Came up with both the simple fix and the more comprehensive one.

Implementation
==============

lib/lp/blueprints/interfaces/specificationtarget.py
lib/lp/registry/interfaces/milestones.py
lib/lp/registry/interfaces/productseries.py
-------------------------------------------

Added doNotSnapshot to the CollectionFields in IHasSpecifications and IHasMilestones. doNotSnapshot had to be added to IProductSeries milestone CollectionFields directly, b/c the interface redefines those fields, but they still connect to probable causes of the Shortlist error.

lib/lp/blueprints/tests/test_hasspecifications.py
lib/lp/registry/tests/test_milestone.py
lib/lp/registry/tests/test_productseries.py
-------------------------------------------

Added tests.

Test
====

bin/test -t test_productseries
bin/test -t test_milestone
bin/test -t test_hasspecifications

QA
==

On staging, hit the +edit or +configure* links for Launchpad. It should not error with shortlist

Lint
====

make lint output:

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/blueprints/interfaces/specificationtarget.py
  lib/lp/blueprints/tests/test_hasspecifications.py
  lib/lp/registry/interfaces/milestone.py
  lib/lp/registry/interfaces/productseries.py
  lib/lp/registry/tests/test_milestone.py
  lib/lp/registry/tests/test_productseries.py

---------------------------------------------------

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (6.8 KiB)

Hi Jon,

I'm glad to see you address this nuisance. You'll make all our lives easier. Danilo was planning to look into it as well this week, since we were getting some of the errors on Translations, but this may just save him the time.

The big question I have though is: could this change confuse clients written to our 1.0 API? We made a promise to keep that API stable. Or do we consider it a plain bug in the client if it can't cope with a collection no longer being snapshotted?

I'm voting "needs fixing," but please don't take that as a vote of no confidence in your approach. It's merely that I'm requesting some changes to the test code that would probably warrant a new review round.

> === modified file 'lib/lp/blueprints/tests/test_hasspecifications.py'
> --- lib/lp/blueprints/tests/test_hasspecifications.py 2010-11-29 18:04:07 +0000
> +++ lib/lp/blueprints/tests/test_hasspecifications.py 2011-01-12 13:35:47 +0000

> @@ -178,3 +184,49 @@
> product=product, name="spec3")
> self.assertNamesOfSpecificationsAre(
> ["spec1"], person.valid_specifications)
> +
> +
> +class HasSpecificationsSnapshotTestCase(TestCaseWithFactory):
> + """A TestCase for snapshots of specification targets."""
> +
> + layer = DatabaseFunctionalLayer
> +
> + def setUp(self):
> + super(HasSpecificationsSnapshotTestCase, self).setUp()

This method looks redundant. Might as well leave it out AFAICS.

> + def check_omissions(self, target, providing):

What omissions are being checked, exactly?

The word "omission" has strong negative connotations: it generally implies that something is missing that shouldn't be. If this method is meant to check that collections are _properly omitted_ from the snapshot, maybe there's some better word.

> + """snapshots of objects with IHasSpecification should not snapshot
> + marked attributes.

Please start your sentence with a capital letter!

This is a statement of fact, but the first line of a docstring is expected to describe what the method actually does for the caller.

> + wrap an export with 'donotsnapshot' to force the snapshot to not
> + include that attribute.

Please start your sentence with a capital letter!

Is the imperative an instruction to this method's caller? Or a description of what the method does in the imperative form? Or general advice to programmers?

> + """
> + snapshot = Snapshot(target, providing=providing)

It's probably just my shallow understanding of Zope, but I would have expected "providing=IHasSpecifications" to work here.

> + omitted = [
> + 'all_specifications',
> + 'valid_specifications',
> + ]
> + for attribute in omitted:
> + self.assertFalse(
> + hasattr(snapshot, attribute),
> + "snapshot should not include %s." % attribute)

Please start your sentence with a capital letter!

> === modified file 'lib/lp/registry/tests/test_milestone.py'
> --- lib/lp/registry/tests/test_milestone.py 2010-10-04 19:50:45 +0000
> +++ lib/lp/registry/tests/test_milestone.py 2011-01-12 13:35:47 +0000

> @@ -81,6 +91,48 @@
...

Read more...

review: Needs Fixing
Revision history for this message
j.c.sackett (jcsackett) wrote :
Download full text (5.3 KiB)

> Danilo was planning to look into it as well this week, since we were getting
> some of the errors on Translations, but this may just save him the time.

Danilo and I actually talked about this yesterday; my branch incorporates the fix he had as well.

> The big question I have though is: could this change confuse clients written
> to our 1.0 API? We made a promise to keep that API stable. Or do we consider
> it a plain bug in the client if it can't cope with a collection no longer
> being snapshotted?

I don't believe this will be an issue--CollectionFields at large can still be snapshotted; these particular fields just are no longer included. I don't see how that would effect a client, but I may be missing something?

> > === modified file 'lib/lp/blueprints/tests/test_hasspecifications.py'
> > --- lib/lp/blueprints/tests/test_hasspecifications.py 2010-11-29 18:04:07
> +0000
> > +++ lib/lp/blueprints/tests/test_hasspecifications.py 2011-01-12 13:35:47
> +0000
>
> > @@ -178,3 +184,49 @@
> > product=product, name="spec3")
> > self.assertNamesOfSpecificationsAre(
> > ["spec1"], person.valid_specifications)
> > +
> > +
> > +class HasSpecificationsSnapshotTestCase(TestCaseWithFactory):
> > + """A TestCase for snapshots of specification targets."""
> > +
> > + layer = DatabaseFunctionalLayer
> > +
> > + def setUp(self):
> > + super(HasSpecificationsSnapshotTestCase, self).setUp()
>
> This method looks redundant. Might as well leave it out AFAICS.

For TestCaseWithFactory descendants, you have to construct it this way. I'm not certain why, but in the past these classes fail otherwise. You might also see code that calls it in the parent setUp directly in the in __init__ instead.

> > + def check_omissions(self, target, providing):
>
> What omissions are being checked, exactly?
>
> The word "omission" has strong negative connotations: it generally implies
> that something is missing that shouldn't be. If this method is meant to check
> that collections are _properly omitted_ from the snapshot, maybe there's some
> better word.

It's checking to make sure that the correct fields are omitted from the snapshot. I suppose I can use skipped instead, if omitted is too negative.

> > + snapshot = Snapshot(target, providing=providing)
>
> It's probably just my shallow understanding of Zope, but I would have expected
> "providing=IHasSpecifications" to work here.

It might, but the test is checking the workflow for a snapshot of the interface normally snapshotted, so providing can be (and is, in each test) IProduct, IDistribution, etc. I thought it made for a better test this way, but doing it the way you suggest would decouple it from other interfaces, so I'll make that change.

> > === modified file 'lib/lp/registry/tests/test_milestone.py'
> > --- lib/lp/registry/tests/test_milestone.py 2010-10-04 19:50:45 +0000
> > +++ lib/lp/registry/tests/test_milestone.py 2011-01-12 13:35:47 +0000
>
> > @@ -81,6 +91,48 @@
> > [1, 2, 3])
> >
> >
> > +class HasMilestonesSnapshotTestCase(TestCaseWithFactory):
> > + """A TestCase for snapshots of pillars with milestones."""
> > +
>...

Read more...

Revision history for this message
j.c.sackett (jcsackett) wrote :

I have made the changes outlined above.

I don't believe that this will be an API incompatibility.

1=== modified file 'lib/lp/blueprints/tests/test_hasspecifications.py'
2--- lib/lp/blueprints/tests/test_hasspecifications.py 2011-01-11 22:53:57 +0000
3+++ lib/lp/blueprints/tests/test_hasspecifications.py 2011-01-12 18:05:13 +0000
4@@ -5,18 +5,15 @@
5
6 __metaclass__ = type
7
8-from lazr.lifecycle.snapshot import Snapshot
9-
10 from canonical.testing.layers import DatabaseFunctionalLayer
11 from lp.blueprints.interfaces.specification import (
12 SpecificationDefinitionStatus,
13 )
14-from lp.registry.interfaces.product import IProduct
15-from lp.registry.interfaces.distribution import IDistribution
16-from lp.registry.interfaces.distroseries import IDistroSeries
17-from lp.registry.interfaces.productseries import IProductSeries
18-from lp.registry.interfaces.projectgroup import IProjectGroup
19+from lp.blueprints.interfaces.specificationtarget import (
20+ IHasSpecifications,
21+ )
22 from lp.testing import TestCaseWithFactory
23+from lp.testing.matcher import DoesNotSnapshot
24
25
26 class HasSpecificationsTests(TestCaseWithFactory):
27@@ -191,42 +188,30 @@
28
29 layer = DatabaseFunctionalLayer
30
31- def setUp(self):
32- super(HasSpecificationsSnapshotTestCase, self).setUp()
33-
34- def check_omissions(self, target, providing):
35- """snapshots of objects with IHasSpecification should not snapshot
36- marked attributes.
37-
38- wrap an export with 'donotsnapshot' to force the snapshot to not
39- include that attribute.
40- """
41- snapshot = Snapshot(target, providing=providing)
42- omitted = [
43+ def check_skipped(self, target, providing):
44+ """Asserts that fields marked doNotSnapshot are skipped."""
45+ skipped = [
46 'all_specifications',
47 'valid_specifications',
48 ]
49- for attribute in omitted:
50- self.assertFalse(
51- hasattr(snapshot, attribute),
52- "snapshot should not include %s." % attribute)
53+ self.assertThat(target, DoesNotSnapshot(skipped, IHasSpecifications))
54
55 def test_product(self):
56 product = self.factory.makeProduct()
57- self.check_omissions(product, IProduct)
58+ self.check_skipped(product)
59
60 def test_distribution(self):
61 distribution = self.factory.makeDistribution()
62- self.check_omissions(distribution, IDistribution)
63+ self.check_skipped(distribution)
64
65 def test_productseries(self):
66 productseries = self.factory.makeProductSeries()
67- self.check_omissions(productseries, IProductSeries)
68+ self.check_skipped(productseries)
69
70 def test_distroseries(self):
71 distroseries = self.factory.makeDistroSeries()
72- self.check_omissions(distroseries, IDistroSeries)
73+ self.check_skipped(distroseries)
74
75 def test_projectgroup(self):
76 projectgroup = self.factory.makeProject()
77- self.check_omissions(projectgroup, IProjectGroup)
78+ self.check_skipped(projectgroup)
79
80=== modified file 'lib/lp/registry/tests/test_milestone.py'
81--- lib/lp/registry/tests/test_milestone.py 2011-01-11 23:43:04 +0000
82+++ lib/lp/registry/tests/test_milestone.py 2011-01-12 18:05:13 +0000
83@@ -7,7 +7,6 @@
84
85 import unittest
86
87-from lazr.lifecycle.snapshot import Snapshot
88 from zope.component import getUtility
89
90 from canonical.launchpad.ftests import (
91@@ -21,15 +20,13 @@
92 )
93 from lp.app.errors import NotFoundError
94 from lp.registry.interfaces.distribution import IDistributionSet
95-from lp.registry.interfaces.distribution import IDistribution
96-from lp.registry.interfaces.distroseries import IDistroSeries
97-from lp.registry.interfaces.milestone import IMilestoneSet
98-from lp.registry.interfaces.product import (
99- IProduct,
100- IProductSet,
101+from lp.registry.interfaces.milestone import (
102+ IHasMilestones,
103+ IMilestoneSet,
104 )
105-from lp.registry.interfaces.projectgroup import IProjectGroup
106+from lp.registry.interfaces.product import IProductSet
107 from lp.testing import TestCaseWithFactory
108+from lp.testing.matchers import DoesNotSnapshot
109
110
111 class MilestoneTest(unittest.TestCase):
112@@ -96,47 +93,26 @@
113
114 layer = DatabaseFunctionalLayer
115
116- def setUp(self):
117- super(HasMilestonesSnapshotTestCase, self).setUp()
118-
119- def check_omissions(self, target, providing):
120- """snapshots of objects with IHasSpecification should not snapshot
121- marked attributes.
122-
123- wrap an export with 'donotsnapshot' to force the snapshot to not
124- include that attribute.
125- """
126- snapshot = Snapshot(target, providing=providing)
127- omitted = [
128+ def check_skipped(self, target):
129+ """Asserts that fields marked doNotSnapshot are skipped."""
130+ skipped = [
131 'milestones',
132 'all_milestones',
133 ]
134- for attribute in omitted:
135- self.assertFalse(
136- hasattr(snapshot, attribute),
137- "snapshot should not include %s." % attribute)
138+ self.assertThat(target, DoesNotSnapshot(skipped, IHasMilestones))
139
140 def test_product(self):
141 product = self.factory.makeProduct()
142- self.check_omissions(product, IProduct)
143+ self.check_skipped(product)
144
145 def test_distribution(self):
146 distribution = self.factory.makeDistribution()
147- self.check_omissions(distribution, IDistribution)
148+ self.check_skipped(distribution)
149
150 def test_distroseries(self):
151 distroseries = self.factory.makeDistroSeries()
152- self.check_omissions(distroseries, IDistroSeries)
153+ self.check_skipped(distroseries)
154
155 def test_projectgroup(self):
156 projectgroup = self.factory.makeProject()
157- self.check_omissions(projectgroup, IProjectGroup)
158-
159-
160-def test_suite():
161- """Return the test suite for the tests in this module."""
162- return unittest.TestLoader().loadTestsFromName(__name__)
163-
164-
165-if __name__ == '__main__':
166- unittest.main()
167+ self.check_skipped(projectgroup)
168
169=== modified file 'lib/lp/registry/tests/test_productseries.py'
170--- lib/lp/registry/tests/test_productseries.py 2011-01-11 22:53:57 +0000
171+++ lib/lp/registry/tests/test_productseries.py 2011-01-12 18:05:13 +0000
172@@ -5,9 +5,6 @@
173
174 __metaclass__ = type
175
176-from unittest import TestLoader
177-
178-from lazr.lifecycle.snapshot import Snapshot
179 from zope.component import getUtility
180
181 from canonical.launchpad.ftests import login
182@@ -23,6 +20,7 @@
183 IProductSeriesSet,
184 )
185 from lp.testing import TestCaseWithFactory
186+from lp.testing.matchers import DoesNotSnapshot
187 from lp.translations.interfaces.translations import (
188 TranslationsBranchImportMode,
189 )
190@@ -310,27 +308,13 @@
191
192 layer = DatabaseFunctionalLayer
193
194- def setUp(self):
195- super(ProductSeriesSnapshotTestCase, self).setUp()
196-
197 def test_productseries(self):
198- """snapshots of objects with IHasSpecification should not snapshot
199- marked attributes.
200-
201- wrap an export with 'donotsnapshot' to force the snapshot to not
202- include that attribute.
203- """
204+ """Asserts that fields marked doNotSnapshot are skipped."""
205 productseries = self.factory.makeProductSeries()
206- snapshot = Snapshot(productseries, providing=IProductSeries)
207- omitted = [
208+ skipped = [
209 'milestones',
210 'all_milestones',
211 ]
212- for attribute in omitted:
213- self.assertFalse(
214- hasattr(snapshot, attribute),
215- "snapshot should not include %s." % attribute)
216-
217-
218-def test_suite():
219- return TestLoader().loadTestsFromName(__name__)
220+ self.assertThat(
221+ productseries,
222+ DoesNotSnapshot(skipped, IProductSeries))
223
224=== modified file 'lib/lp/testing/matchers.py'
225--- lib/lp/testing/matchers.py 2010-12-13 21:40:56 +0000
226+++ lib/lp/testing/matchers.py 2011-01-12 18:05:13 +0000
227@@ -13,6 +13,7 @@
228 'ProvidesAndIsProxied',
229 ]
230
231+from lazr.lifecycle.snapshot import Snapshot
232 from testtools.content import Content
233 from testtools.content_type import UTF8_TEXT
234 from testtools.matchers import (
235@@ -141,7 +142,7 @@
236 result = []
237 for query in self.query_collector.queries:
238 result.append(unicode(query).encode('utf8'))
239- return {'queries': Content(UTF8_TEXT, lambda:['\n'.join(result)])}
240+ return {'queries': Content(UTF8_TEXT, lambda: ['\n'.join(result)])}
241
242
243 class IsNotProxied(Mismatch):
244@@ -265,3 +266,38 @@
245 mismatches.append(mismatch)
246 if mismatches:
247 return MismatchesAll(mismatches)
248+
249+
250+class WasSnapshotted(Mismatch):
251+
252+ def __init__(self, matchee, attribute):
253+ self.matchee = matchee
254+ self.attribute = attribute
255+
256+ def describe(self):
257+ return "Snapshot of %s should not include %s" % (
258+ self.matchee, self.attribute)
259+
260+
261+class DoesNotSnapshot(Matcher):
262+
263+ def __init__(self, attr_list, interface, error_msg=None):
264+ self.attr_list = attr_list
265+ self.interface = interface
266+ self.error_msg = error_msg
267+
268+ def __str__(self):
269+ return "Does not include %s when Snapshot is provided %s." % (
270+ ', '.join(self.attr_list), self.interface)
271+
272+ def match(self, matchee):
273+ snapshot = Snapshot(matchee, providing=self.interface)
274+ mismatches = list()
275+ for attribute in self.attr_list:
276+ if hasattr(snapshot, attribute):
277+ mismatches.append(WasSnapshotted(matchee, attribute))
278+
279+ if mismatches == []:
280+ return None
281+ else:
282+ return MismatchesAll(mismatches)
Revision history for this message
Benji York (benji) wrote :

I believe you've addressed (and well) all of Jeroen's comments. I have
only two minor tweaks to suggest:

The use of "list()" to construct an empty list on line 274 of your diff
is a bit non-idiomatic. The list literal is more common.

The style guide (https://dev.launchpad.net/PythonStyleGuide#Truth%20conditionals)
prefers "if len(mismatches) == 0:" over "if mismatches == []:" for the
expression on line 279.

With those two changes your incremental diff looks great.

review: Approve (code*)
Revision history for this message
j.c.sackett (jcsackett) wrote :

I'll make those two tweaks.

Also, I have confirmed with Leonard Richardson that this branch shouldn't cause API incompatability.

3:05 PM leonardr
jcsackett: when an entry is changed, we take a snapshot before making the change, and send the snapshot along with the new object in an ObjectModifiedEvent

3:05 PM leonardr
so that anyone who cares about the change can see what changed

3:05 PM
afaik that's the only place where lazr.restful uses snapshots

3:05 PM leonardr
and you can't change a collection field

3:05 PM
so unless i'm missing something, this should not change the behavior of the web service

Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :

> I believe you've addressed (and well) all of Jeroen's comments. I have
> only two minor tweaks to suggest:
>
> The use of "list()" to construct an empty list on line 274 of your diff
> is a bit non-idiomatic. The list literal is more common.
>
> The style guide
> (https://dev.launchpad.net/PythonStyleGuide#Truth%20conditionals)
> prefers "if len(mismatches) == 0:" over "if mismatches == []:" for the
> expression on line 279.
>
> With those two changes your incremental diff looks great.

Hi JC,

I agree with Benji's comments. Also, the test_hasspecifications.py has a couple of minor issues that are breaking it.

Looks good.

-Edwin

review: Approve (code)
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Now that's team work! Benji, good review.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/blueprints/interfaces/specificationtarget.py'
2--- lib/lp/blueprints/interfaces/specificationtarget.py 2010-11-25 19:09:11 +0000
3+++ lib/lp/blueprints/interfaces/specificationtarget.py 2011-01-13 14:37:26 +0000
4@@ -19,6 +19,7 @@
5 )
6 from zope.schema import TextLine
7
8+from lazr.lifecycle.snapshot import doNotSnapshot
9 from lazr.restful.declarations import (
10 exported,
11 export_as_webservice_entry,
12@@ -42,21 +43,21 @@
13 associated with them, and you can use this interface to query those.
14 """
15
16- all_specifications = exported(
17+ all_specifications = exported(doNotSnapshot(
18 CollectionField(
19 title=_("All specifications"),
20 value_type=Reference(schema=Interface), # ISpecification, really.
21 readonly=True,
22 description=_(
23 'A list of all specifications, regardless of status or '
24- 'approval or completion, for this object.')),
25+ 'approval or completion, for this object.'))),
26 ('devel', dict(exported=True)), exported=False)
27
28 has_any_specifications = Attribute(
29 'A true or false indicator of whether or not this object has any '
30 'specifications associated with it, regardless of their status.')
31
32- valid_specifications = exported(
33+ valid_specifications = exported(doNotSnapshot(
34 CollectionField(
35 title=_("Valid specifications"),
36 value_type=Reference(schema=Interface), # ISpecification, really.
37@@ -64,7 +65,7 @@
38 description=_(
39 'All specifications that are not obsolete. When called from '
40 'an ISpecificationGoal it will also exclude the ones that '
41- 'have not been accepted for that goal')),
42+ 'have not been accepted for that goal'))),
43 ('devel', dict(exported=True)), exported=False)
44
45 latest_specifications = Attribute(
46
47=== modified file 'lib/lp/blueprints/tests/test_hasspecifications.py'
48--- lib/lp/blueprints/tests/test_hasspecifications.py 2010-11-29 18:04:07 +0000
49+++ lib/lp/blueprints/tests/test_hasspecifications.py 2011-01-13 14:37:26 +0000
50@@ -5,12 +5,15 @@
51
52 __metaclass__ = type
53
54-
55 from canonical.testing.layers import DatabaseFunctionalLayer
56 from lp.blueprints.interfaces.specification import (
57 SpecificationDefinitionStatus,
58 )
59+from lp.blueprints.interfaces.specificationtarget import (
60+ IHasSpecifications,
61+ )
62 from lp.testing import TestCaseWithFactory
63+from lp.testing.matchers import DoesNotSnapshot
64
65
66 class HasSpecificationsTests(TestCaseWithFactory):
67@@ -178,3 +181,37 @@
68 product=product, name="spec3")
69 self.assertNamesOfSpecificationsAre(
70 ["spec1"], person.valid_specifications)
71+
72+
73+class HasSpecificationsSnapshotTestCase(TestCaseWithFactory):
74+ """A TestCase for snapshots of specification targets."""
75+
76+ layer = DatabaseFunctionalLayer
77+
78+ def check_skipped(self, target):
79+ """Asserts that fields marked doNotSnapshot are skipped."""
80+ skipped = [
81+ 'all_specifications',
82+ 'valid_specifications',
83+ ]
84+ self.assertThat(target, DoesNotSnapshot(skipped, IHasSpecifications))
85+
86+ def test_product(self):
87+ product = self.factory.makeProduct()
88+ self.check_skipped(product)
89+
90+ def test_distribution(self):
91+ distribution = self.factory.makeDistribution()
92+ self.check_skipped(distribution)
93+
94+ def test_productseries(self):
95+ productseries = self.factory.makeProductSeries()
96+ self.check_skipped(productseries)
97+
98+ def test_distroseries(self):
99+ distroseries = self.factory.makeDistroSeries()
100+ self.check_skipped(distroseries)
101+
102+ def test_projectgroup(self):
103+ projectgroup = self.factory.makeProject()
104+ self.check_skipped(projectgroup)
105
106=== modified file 'lib/lp/registry/interfaces/milestone.py'
107--- lib/lp/registry/interfaces/milestone.py 2010-12-01 22:26:50 +0000
108+++ lib/lp/registry/interfaces/milestone.py 2011-01-13 14:37:26 +0000
109@@ -15,6 +15,7 @@
110 'IProjectGroupMilestone',
111 ]
112
113+from lazr.lifecycle.snapshot import doNotSnapshot
114 from lazr.restful.declarations import (
115 call_with,
116 export_as_webservice_entry,
117@@ -39,7 +40,6 @@
118 from zope.schema import (
119 Bool,
120 Choice,
121- Date,
122 Int,
123 TextLine,
124 )
125@@ -255,18 +255,18 @@
126
127 has_milestones = Bool(title=_("Whether the object has any milestones."))
128
129- milestones = exported(
130+ milestones = exported(doNotSnapshot(
131 CollectionField(
132 title=_("The visible and active milestones associated with this "
133 "object, ordered by date expected."),
134- value_type=Reference(schema=IMilestone)),
135+ value_type=Reference(schema=IMilestone))),
136 exported_as='active_milestones')
137
138- all_milestones = exported(
139+ all_milestones = exported(doNotSnapshot(
140 CollectionField(
141 title=_("All milestones associated with this object, ordered by "
142 "date expected."),
143- value_type=Reference(schema=IMilestone)))
144+ value_type=Reference(schema=IMilestone))))
145
146
147 class ICanGetMilestonesDirectly(Interface):
148
149=== modified file 'lib/lp/registry/interfaces/productseries.py'
150--- lib/lp/registry/interfaces/productseries.py 2010-11-27 05:40:38 +0000
151+++ lib/lp/registry/interfaces/productseries.py 2011-01-13 14:37:26 +0000
152@@ -16,6 +16,7 @@
153 'ITimelineProductSeries',
154 ]
155
156+from lazr.lifecycle.snapshot import doNotSnapshot
157 from lazr.restful.declarations import (
158 export_as_webservice_entry,
159 export_factory_operation,
160@@ -212,20 +213,20 @@
161 sourcepackages = Attribute(_("List of distribution packages for this "
162 "product series"))
163
164- milestones = exported(
165+ milestones = exported(doNotSnapshot(
166 CollectionField(
167 title=_("The visible milestones associated with this "
168 "project series, ordered by date expected."),
169 readonly=True,
170- value_type=Reference(schema=IMilestone)),
171+ value_type=Reference(schema=IMilestone))),
172 exported_as='active_milestones')
173
174- all_milestones = exported(
175+ all_milestones = exported(doNotSnapshot(
176 CollectionField(
177 title=_("All milestones associated with this project series, "
178 "ordered by date expected."),
179 readonly=True,
180- value_type=Reference(schema=IMilestone)))
181+ value_type=Reference(schema=IMilestone))))
182
183 branch = exported(
184 ReferenceChoice(
185
186=== modified file 'lib/lp/registry/tests/test_milestone.py'
187--- lib/lp/registry/tests/test_milestone.py 2010-10-04 19:50:45 +0000
188+++ lib/lp/registry/tests/test_milestone.py 2011-01-13 14:37:26 +0000
189@@ -14,12 +14,19 @@
190 login,
191 logout,
192 )
193-from canonical.testing.layers import LaunchpadFunctionalLayer
194-
195+from canonical.testing.layers import (
196+ DatabaseFunctionalLayer,
197+ LaunchpadFunctionalLayer,
198+ )
199 from lp.app.errors import NotFoundError
200 from lp.registry.interfaces.distribution import IDistributionSet
201-from lp.registry.interfaces.milestone import IMilestoneSet
202+from lp.registry.interfaces.milestone import (
203+ IHasMilestones,
204+ IMilestoneSet,
205+ )
206 from lp.registry.interfaces.product import IProductSet
207+from lp.testing import TestCaseWithFactory
208+from lp.testing.matchers import DoesNotSnapshot
209
210
211 class MilestoneTest(unittest.TestCase):
212@@ -81,10 +88,31 @@
213 [1, 2, 3])
214
215
216-def test_suite():
217- """Return the test suite for the tests in this module."""
218- return unittest.TestLoader().loadTestsFromName(__name__)
219-
220-
221-if __name__ == '__main__':
222- unittest.main()
223+class HasMilestonesSnapshotTestCase(TestCaseWithFactory):
224+ """A TestCase for snapshots of pillars with milestones."""
225+
226+ layer = DatabaseFunctionalLayer
227+
228+ def check_skipped(self, target):
229+ """Asserts that fields marked doNotSnapshot are skipped."""
230+ skipped = [
231+ 'milestones',
232+ 'all_milestones',
233+ ]
234+ self.assertThat(target, DoesNotSnapshot(skipped, IHasMilestones))
235+
236+ def test_product(self):
237+ product = self.factory.makeProduct()
238+ self.check_skipped(product)
239+
240+ def test_distribution(self):
241+ distribution = self.factory.makeDistribution()
242+ self.check_skipped(distribution)
243+
244+ def test_distroseries(self):
245+ distroseries = self.factory.makeDistroSeries()
246+ self.check_skipped(distroseries)
247+
248+ def test_projectgroup(self):
249+ projectgroup = self.factory.makeProject()
250+ self.check_skipped(projectgroup)
251
252=== modified file 'lib/lp/registry/tests/test_productseries.py'
253--- lib/lp/registry/tests/test_productseries.py 2010-10-18 20:40:05 +0000
254+++ lib/lp/registry/tests/test_productseries.py 2011-01-13 14:37:26 +0000
255@@ -5,8 +5,6 @@
256
257 __metaclass__ = type
258
259-from unittest import TestLoader
260-
261 from zope.component import getUtility
262
263 from canonical.launchpad.ftests import login
264@@ -17,8 +15,12 @@
265 )
266 from lp.registry.interfaces.distribution import IDistributionSet
267 from lp.registry.interfaces.distroseries import IDistroSeriesSet
268-from lp.registry.interfaces.productseries import IProductSeriesSet
269+from lp.registry.interfaces.productseries import (
270+ IProductSeries,
271+ IProductSeriesSet,
272+ )
273 from lp.testing import TestCaseWithFactory
274+from lp.testing.matchers import DoesNotSnapshot
275 from lp.translations.interfaces.translations import (
276 TranslationsBranchImportMode,
277 )
278@@ -301,5 +303,18 @@
279 self.productseries.getLatestRelease())
280
281
282-def test_suite():
283- return TestLoader().loadTestsFromName(__name__)
284+class ProductSeriesSnapshotTestCase(TestCaseWithFactory):
285+ """A TestCase for snapshots of productseries."""
286+
287+ layer = DatabaseFunctionalLayer
288+
289+ def test_productseries(self):
290+ """Asserts that fields marked doNotSnapshot are skipped."""
291+ productseries = self.factory.makeProductSeries()
292+ skipped = [
293+ 'milestones',
294+ 'all_milestones',
295+ ]
296+ self.assertThat(
297+ productseries,
298+ DoesNotSnapshot(skipped, IProductSeries))
299
300=== modified file 'lib/lp/testing/matchers.py'
301--- lib/lp/testing/matchers.py 2010-12-13 21:40:56 +0000
302+++ lib/lp/testing/matchers.py 2011-01-13 14:37:26 +0000
303@@ -13,6 +13,7 @@
304 'ProvidesAndIsProxied',
305 ]
306
307+from lazr.lifecycle.snapshot import Snapshot
308 from testtools.content import Content
309 from testtools.content_type import UTF8_TEXT
310 from testtools.matchers import (
311@@ -141,7 +142,7 @@
312 result = []
313 for query in self.query_collector.queries:
314 result.append(unicode(query).encode('utf8'))
315- return {'queries': Content(UTF8_TEXT, lambda:['\n'.join(result)])}
316+ return {'queries': Content(UTF8_TEXT, lambda: ['\n'.join(result)])}
317
318
319 class IsNotProxied(Mismatch):
320@@ -265,3 +266,39 @@
321 mismatches.append(mismatch)
322 if mismatches:
323 return MismatchesAll(mismatches)
324+
325+
326+class WasSnapshotted(Mismatch):
327+
328+ def __init__(self, matchee, attribute):
329+ self.matchee = matchee
330+ self.attribute = attribute
331+
332+ def describe(self):
333+ return "Snapshot of %s should not include %s" % (
334+ self.matchee, self.attribute)
335+
336+
337+class DoesNotSnapshot(Matcher):
338+ """Checks that certain fields are skipped on Snapshots."""
339+
340+ def __init__(self, attr_list, interface, error_msg=None):
341+ self.attr_list = attr_list
342+ self.interface = interface
343+ self.error_msg = error_msg
344+
345+ def __str__(self):
346+ return "Does not include %s when Snapshot is provided %s." % (
347+ ', '.join(self.attr_list), self.interface)
348+
349+ def match(self, matchee):
350+ snapshot = Snapshot(matchee, providing=self.interface)
351+ mismatches = []
352+ for attribute in self.attr_list:
353+ if hasattr(snapshot, attribute):
354+ mismatches.append(WasSnapshotted(matchee, attribute))
355+
356+ if len(mismatches) == 0:
357+ return None
358+ else:
359+ return MismatchesAll(mismatches)