Merge lp:~cjwatson/launchpad/refactor-bugtask-tests into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 17599
Proposed branch: lp:~cjwatson/launchpad/refactor-bugtask-tests
Merge into: lp:launchpad
Diff against target: 321 lines (+67/-108)
5 files modified
lib/lp/app/interfaces/launchpad.py (+3/-0)
lib/lp/app/utilities/celebrities.py (+9/-0)
lib/lp/bugs/browser/tests/test_bugtask.py (+43/-104)
lib/lp/soyuz/browser/tests/test_archive_packages.py (+1/-3)
lib/lp/testing/__init__.py (+11/-1)
To merge this branch: bzr merge lp:~cjwatson/launchpad/refactor-bugtask-tests
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+263310@code.launchpad.net

Commit message

Refactor several bugtask browser tests to use record_two_runs, improving test isolation.

Description of the change

Refactor several bugtask browser tests to use record_two_runs rather than similar inline code that didn't do as good a job of isolating tests.

I've improved record_two_runs in a couple of ways in the process to make it even more accurate and easy to use. It now clears celebrity descriptors as well, and it gains an optional login_method argument which is useful in browser tests (since setupBrowserForUser calls logout).

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/app/interfaces/launchpad.py'
2--- lib/lp/app/interfaces/launchpad.py 2014-11-28 22:28:40 +0000
3+++ lib/lp/app/interfaces/launchpad.py 2015-06-30 08:37:26 +0000
4@@ -73,6 +73,9 @@
5 """Return true if there is an IPerson celebrity with the given name.
6 """
7
8+ def clearCache():
9+ """Clear any cached celebrities."""
10+
11
12 class IServiceUsage(Interface):
13 """Pillar service usages."""
14
15=== modified file 'lib/lp/app/utilities/celebrities.py'
16--- lib/lp/app/utilities/celebrities.py 2013-05-10 06:28:17 +0000
17+++ lib/lp/app/utilities/celebrities.py 2015-06-30 08:37:26 +0000
18@@ -176,4 +176,13 @@
19 return mirror
20
21 def isCelebrityPerson(self, name):
22+ """See `ILaunchpadCelebrities`."""
23 return str(name) in PersonCelebrityDescriptor.names
24+
25+ @classmethod
26+ def clearCache(cls):
27+ """See `ILaunchpadCelebrities`."""
28+ for name in cls.__dict__:
29+ desc = getattr(cls, name)
30+ if isinstance(desc, CelebrityDescriptor):
31+ desc.id = None
32
33=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
34--- lib/lp/bugs/browser/tests/test_bugtask.py 2015-06-25 07:39:40 +0000
35+++ lib/lp/bugs/browser/tests/test_bugtask.py 2015-06-30 08:37:26 +0000
36@@ -17,8 +17,8 @@
37 from pytz import UTC
38 import simplejson
39 import soupmatchers
40-from storm.store import Store
41 from testtools.matchers import (
42+ Equals,
43 LessThan,
44 Not,
45 )
46@@ -80,9 +80,9 @@
47 login,
48 login_person,
49 person_logged_in,
50+ record_two_runs,
51 TestCaseWithFactory,
52 )
53-from lp.testing._webservice import QueryCollector
54 from lp.testing.layers import (
55 DatabaseFunctionalLayer,
56 LaunchpadFunctionalLayer,
57@@ -115,65 +115,29 @@
58
59 layer = LaunchpadFunctionalLayer
60
61- def invalidate_caches(self, obj):
62- store = Store.of(obj)
63- # Make sure everything is in the database.
64- store.flush()
65- # And invalidate the cache (not a reset, because that stops us using
66- # the domain objects)
67- store.invalidate()
68-
69 def test_rendered_query_counts_constant_with_team_memberships(self):
70- login(ADMIN_EMAIL)
71 task = self.factory.makeBugTask()
72- person_no_teams = self.factory.makePerson()
73- person_with_teams = self.factory.makePerson()
74- for _ in range(10):
75- self.factory.makeTeam(members=[person_with_teams])
76- # count with no teams
77+ person = self.factory.makePerson()
78 url = canonical_url(task)
79- recorder = QueryCollector()
80- recorder.register()
81- self.addCleanup(recorder.unregister)
82- self.invalidate_caches(task)
83- self.getUserBrowser(url, person_no_teams)
84+ recorder1, recorder2 = record_two_runs(
85+ lambda: self.getUserBrowser(url, person),
86+ lambda: self.factory.makeTeam(members=[person]),
87+ 0, 10, login_method=lambda: login(ADMIN_EMAIL))
88 # This may seem large: it is; there is easily another 25% fat in
89 # there.
90- # If this test is run in isolation, the query count is 80.
91- # Other tests in this TestCase could cache the
92- # "SELECT id, product, project, distribution FROM PillarName ..."
93- # query by previously browsing the task url, in which case the
94- # query count is decreased by one.
95- self.assertThat(recorder, HasQueryCount(LessThan(83)))
96- count_with_no_teams = recorder.count
97- # count with many teams
98- self.invalidate_caches(task)
99- self.getUserBrowser(url, person_with_teams)
100- # Allow an increase of one because storm bug 619017 causes additional
101- # queries, revalidating things unnecessarily. An increase which is
102- # less than the number of new teams shows it is definitely not
103- # growing per-team.
104- self.assertThat(recorder, HasQueryCount(
105- LessThan(count_with_no_teams + 3),
106- ))
107+ self.assertThat(recorder1, HasQueryCount(LessThan(81)))
108+ self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
109
110 def test_rendered_query_counts_constant_with_attachments(self):
111- with celebrity_logged_in('admin'):
112- browses_under_limit = BrowsesWithQueryLimit(
113- 85, self.factory.makePerson())
114-
115- # First test with a single attachment.
116- task = self.factory.makeBugTask()
117- self.factory.makeBugAttachment(bug=task.bug)
118- self.assertThat(task, browses_under_limit)
119-
120- with celebrity_logged_in('admin'):
121- # And now with 10.
122- task = self.factory.makeBugTask()
123- self.factory.makeBugTask(bug=task.bug)
124- for i in range(10):
125- self.factory.makeBugAttachment(bug=task.bug)
126- self.assertThat(task, browses_under_limit)
127+ task = self.factory.makeBugTask()
128+ person = self.factory.makePerson()
129+ url = canonical_url(task)
130+ recorder1, recorder2 = record_two_runs(
131+ lambda: self.getUserBrowser(url, person),
132+ lambda: self.factory.makeBugAttachment(bug=task.bug),
133+ 1, 9, login_method=lambda: login(ADMIN_EMAIL))
134+ self.assertThat(recorder1, HasQueryCount(LessThan(82)))
135+ self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
136
137 def makeLinkedBranchMergeProposal(self, sourcepackage, bug, owner):
138 with person_logged_in(owner):
139@@ -199,22 +163,19 @@
140 self.factory.makeBugTask(bug=bug, owner=owner, target=sp)
141 task = bug.default_bugtask
142 url = canonical_url(task)
143- recorder = QueryCollector()
144- recorder.register()
145- self.addCleanup(recorder.unregister)
146- self.invalidate_caches(task)
147- self.getUserBrowser(url, owner)
148- # At least 20 of these should be removed.
149- self.assertThat(recorder, HasQueryCount(LessThan(114)))
150- count_with_no_branches = recorder.count
151- for sp in sourcepackages:
152- self.makeLinkedBranchMergeProposal(sp, bug, owner)
153- self.invalidate_caches(task)
154- self.getUserBrowser(url, owner) # This triggers the query recorder.
155+
156+ def make_merge_proposals():
157+ for sp in sourcepackages:
158+ self.makeLinkedBranchMergeProposal(sp, bug, owner)
159+
160+ recorder1, recorder2 = record_two_runs(
161+ lambda: self.getUserBrowser(url, owner),
162+ make_merge_proposals, 0, 1)
163+ self.assertThat(recorder1, HasQueryCount(LessThan(87)))
164 # Ideally this should be much fewer, but this tries to keep a win of
165 # removing more than half of these.
166 self.assertThat(
167- recorder, HasQueryCount(LessThan(count_with_no_branches + 46)))
168+ recorder2, HasQueryCount(LessThan(recorder1.count + 41)))
169
170 def test_interesting_activity(self):
171 # The interesting_activity property returns a tuple of interesting
172@@ -245,27 +206,19 @@
173 def test_rendered_query_counts_constant_with_activities(self):
174 # More queries are not used for extra bug activities.
175 task = self.factory.makeBugTask()
176+ person = self.factory.makePerson()
177+ url = canonical_url(task)
178
179 def add_activity(what, who):
180 getUtility(IBugActivitySet).new(
181 task.bug, datetime.now(UTC), who, whatchanged=what)
182
183- # Render the view with one activity.
184- with celebrity_logged_in('admin'):
185- browses_under_limit = BrowsesWithQueryLimit(
186- 83, self.factory.makePerson())
187- person = self.factory.makePerson()
188- add_activity("description", person)
189-
190- self.assertThat(task, browses_under_limit)
191-
192- # Render the view with many more activities by different people.
193- with celebrity_logged_in('admin'):
194- for _ in range(20):
195- person = self.factory.makePerson()
196- add_activity("description", person)
197-
198- self.assertThat(task, browses_under_limit)
199+ recorder1, recorder2 = record_two_runs(
200+ lambda: self.getUserBrowser(url, person),
201+ lambda: add_activity("description", self.factory.makePerson()),
202+ 1, 20, login_method=lambda: login(ADMIN_EMAIL))
203+ self.assertThat(recorder1, HasQueryCount(LessThan(82)))
204+ self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
205
206 def test_rendered_query_counts_constant_with_milestones(self):
207 # More queries are not used for extra milestones.
208@@ -274,7 +227,7 @@
209
210 with celebrity_logged_in('admin'):
211 browses_under_limit = BrowsesWithQueryLimit(
212- 88, self.factory.makePerson())
213+ 82, self.factory.makePerson())
214
215 self.assertThat(bug, browses_under_limit)
216
217@@ -2046,29 +1999,15 @@
218 view.initialize()
219 return view
220
221- def invalidate_caches(self, obj):
222- store = Store.of(obj)
223- store.flush()
224- store.invalidate()
225-
226 def test_rendered_query_counts_constant_with_many_bugtasks(self):
227 product = self.factory.makeProduct()
228 url = canonical_url(product, view_name='+bugs')
229- bug = self.factory.makeBug(target=product)
230- buggy_product = self.factory.makeProduct()
231- buggy_url = canonical_url(buggy_product, view_name='+bugs')
232- for _ in range(10):
233- self.factory.makeBug(target=buggy_product)
234- recorder = QueryCollector()
235- recorder.register()
236- self.addCleanup(recorder.unregister)
237- self.invalidate_caches(bug)
238- # count with single task
239- self.getUserBrowser(url)
240- self.assertThat(recorder, HasQueryCount(LessThan(36)))
241- # count with many tasks
242- self.getUserBrowser(buggy_url)
243- self.assertThat(recorder, HasQueryCount(LessThan(36)))
244+ recorder1, recorder2 = record_two_runs(
245+ lambda: self.getUserBrowser(url),
246+ lambda: self.factory.makeBug(target=product),
247+ 1, 10, login_method=lambda: login(ANONYMOUS))
248+ self.assertThat(recorder1, HasQueryCount(LessThan(46)))
249+ self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
250
251 def test_mustache_model_in_json(self):
252 """The IJSONRequestCache should contain mustache_model.
253
254=== modified file 'lib/lp/soyuz/browser/tests/test_archive_packages.py'
255--- lib/lp/soyuz/browser/tests/test_archive_packages.py 2013-05-10 06:44:11 +0000
256+++ lib/lp/soyuz/browser/tests/test_archive_packages.py 2015-06-30 08:37:26 +0000
257@@ -447,6 +447,4 @@
258 view.render()
259 recorder1, recorder2 = record_two_runs(
260 ppa_index_render, self.createPackage, 2, 3)
261-
262- self.assertThat(
263- recorder2, HasQueryCount(LessThan(recorder1.count + 2)))
264+ self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
265
266=== modified file 'lib/lp/testing/__init__.py'
267--- lib/lp/testing/__init__.py 2015-01-13 14:07:42 +0000
268+++ lib/lp/testing/__init__.py 2015-06-30 08:37:26 +0000
269@@ -125,6 +125,7 @@
270 )
271 from zope.testing.testrunner.runner import TestResult as ZopeTestResult
272
273+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
274 from lp.app.interfaces.security import IAuthorization
275 from lp.codehosting.vfs import (
276 branch_id_to_path,
277@@ -374,16 +375,21 @@
278
279
280 def record_two_runs(tested_method, item_creator, first_round_number,
281- second_round_number=None):
282+ second_round_number=None, login_method=None):
283 """A helper that returns the two storm statement recorders
284 obtained when running tested_method after having run the
285 method {item_creator} {first_round_number} times and then
286 again after having run the same method {second_round_number}
287 times.
288
289+ If {login_method} is not None, it is called before each batch of
290+ {item_creator} calls.
291+
292 :return: a tuple containing the two recorders obtained by the successive
293 runs.
294 """
295+ if login_method is not None:
296+ login_method()
297 for i in range(first_round_number):
298 item_creator()
299 # Record how many queries are issued when {tested_method} is
300@@ -392,17 +398,21 @@
301 flush_database_caches()
302 if queryInteraction() is not None:
303 clear_permission_cache()
304+ getUtility(ILaunchpadCelebrities).clearCache()
305 with StormStatementRecorder() as recorder1:
306 tested_method()
307 # Run {item_creator} {second_round_number} more times.
308 if second_round_number is None:
309 second_round_number = first_round_number
310+ if login_method is not None:
311+ login_method()
312 for i in range(second_round_number):
313 item_creator()
314 # Record again the number of queries issued.
315 flush_database_caches()
316 if queryInteraction() is not None:
317 clear_permission_cache()
318+ getUtility(ILaunchpadCelebrities).clearCache()
319 with StormStatementRecorder() as recorder2:
320 tested_method()
321 return recorder1, recorder2