~launchpad-pqm/launchpad/devel

14625.2.2 by Colin Watson
Add garbo job to clean up broken SPR.dsc_binaries values.
1
# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
8687.15.18 by Karl Fogel
Add the copyright header block to files under lib/canonical/.
2
# GNU Affero General Public License version 3 (see the file LICENSE).
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
3
4
"""Test the database garbage collector."""
5
6
__metaclass__ = type
7
__all__ = []
8
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
9
from datetime import (
10
    datetime,
11
    timedelta,
12
    )
8758.2.63 by Stuart Bishop
Fix tests to cope with the more complex logging environment
13
import logging
14
from StringIO import StringIO
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
15
import time
16
17
from pytz import UTC
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
18
from storm.expr import (
13581.1.1 by Danilo Segan
Merge gmb's fix for 814576.
19
    In,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
20
    Min,
13581.1.1 by Danilo Segan
Merge gmb's fix for 814576.
21
    Not,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
22
    SQL,
23
    )
4953.7.10 by Stuart Bishop
Format imports
24
from storm.locals import (
25
    Int,
26
    Storm,
27
    )
8303.10.7 by James Henstridge
Add a garbo task to remove stale mailing list subscriptions when the
28
from storm.store import Store
8758.3.20 by Stuart Bishop
Add garbo job to rollup BugSummaryJournal
29
from testtools.matchers import (
30
    Equals,
31
    GreaterThan,
32
    )
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
33
import transaction
7675.177.6 by Stuart Bishop
Garbage collect OpenIDAssociations, allow scripts to correct to the auth store as the correct database user, ensure changing database connection settings in the test suite resets ZStorm so new credentials are used
34
from zope.component import getUtility
8697.25.1 by Guilherme Salgado
New task ran as part of garbo-daily to delete unlinked person entries.
35
from zope.security.proxy import removeSecurityProxy
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
36
13635.1.1 by Ian Booth
Add garbo job to remove old answer contacts
37
from lp.answers.model.answercontact import AnswerContact
8758.2.9 by Stuart Bishop
Tests and fixes for BugNotificationPruner
38
from lp.bugs.model.bugnotification import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
39
    BugNotification,
40
    BugNotificationRecipient,
41
    )
42
from lp.code.bzr import (
43
    BranchFormat,
44
    RepositoryFormat,
45
    )
46
from lp.code.enums import CodeImportResultStatus
11703.1.4 by Tim Penhey
Prune the CodeImportEvents.
47
from lp.code.interfaces.codeimportevent import ICodeImportEventSet
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
48
from lp.code.model.branchjob import (
49
    BranchJob,
50
    BranchUpgradeJob,
51
    )
11703.1.4 by Tim Penhey
Prune the CodeImportEvents.
52
from lp.code.model.codeimportevent import CodeImportEvent
8758.2.9 by Stuart Bishop
Tests and fixes for BugNotificationPruner
53
from lp.code.model.codeimportresult import CodeImportResult
8758.7.3 by Stuart Bishop
Remove failing tests for edge cases that will no longer occur
54
from lp.registry.interfaces.person import IPersonSet
11768.1.2 by Curtis Hovey
Used the deglobber to fix imports in python tests, then formatted them using format-new-and-modified-imports.
55
from lp.scripts.garbo import (
4953.7.10 by Stuart Bishop
Format imports
56
    AntiqueSessionPruner,
8758.2.31 by Stuart Bishop
BulkPruner tests
57
    BulkPruner,
11768.1.2 by Curtis Hovey
Used the deglobber to fix imports in python tests, then formatted them using format-new-and-modified-imports.
58
    DailyDatabaseGarbageCollector,
4953.7.17 by Stuart Bishop
Keep only last 6 authenticated sessions for a user
59
    DuplicateSessionPruner,
8758.3.16 by Stuart Bishop
Fix tests for garbo-frequently.py jobs
60
    FrequentDatabaseGarbageCollector,
11768.1.2 by Curtis Hovey
Used the deglobber to fix imports in python tests, then formatted them using format-new-and-modified-imports.
61
    HourlyDatabaseGarbageCollector,
8758.4.18 by Stuart Bishop
Remove LoginToken rows older than 1 year
62
    LoginTokenPruner,
11768.1.2 by Curtis Hovey
Used the deglobber to fix imports in python tests, then formatted them using format-new-and-modified-imports.
63
    OpenIDConsumerAssociationPruner,
4953.7.10 by Stuart Bishop
Format imports
64
    UnusedSessionPruner,
11768.1.2 by Curtis Hovey
Used the deglobber to fix imports in python tests, then formatted them using format-new-and-modified-imports.
65
    )
14606.3.1 by William Grant
Merge canonical.database into lp.services.database.
66
from lp.services.config import config
14606.3.4 by William Grant
Replace canonical.database usage everywhere, and format-imports.
67
from lp.services.database import sqlbase
14606.3.1 by William Grant
Merge canonical.database into lp.services.database.
68
from lp.services.database.constants import (
69
    ONE_DAY_AGO,
70
    SEVEN_DAYS_AGO,
71
    THIRTY_DAYS_AGO,
72
    UTC_NOW,
73
    )
14578.2.1 by William Grant
Move librarian stuff from canonical.launchpad to lp.services.librarian. canonical.librarian remains untouched.
74
from lp.services.database.lpstorm import IMasterStore
14550.1.1 by Steve Kowalik
Run format-imports over lib/lp and lib/canonical/launchpad
75
from lp.services.identity.interfaces.account import AccountStatus
76
from lp.services.identity.interfaces.emailaddress import EmailAddressStatus
13811.1.1 by Jeroen Vermeulen
More lint.
77
from lp.services.job.model.job import Job
14578.2.1 by William Grant
Move librarian stuff from canonical.launchpad to lp.services.librarian. canonical.librarian remains untouched.
78
from lp.services.librarian.model import TimeLimitedToken
8758.2.63 by Stuart Bishop
Fix tests to cope with the more complex logging environment
79
from lp.services.log.logger import NullHandler
13811.1.1 by Jeroen Vermeulen
More lint.
80
from lp.services.messages.model.message import Message
14452.3.1 by William Grant
Move oauth code to lp.services.oauth.
81
from lp.services.oauth.model import (
82
    OAuthAccessToken,
83
    OAuthNonce,
84
    )
14455.3.4 by William Grant
Move OpenID consumer stuff to lp.services.openid.
85
from lp.services.openid.model.openidconsumer import OpenIDConsumerNonce
14578.2.1 by William Grant
Move librarian stuff from canonical.launchpad to lp.services.librarian. canonical.librarian remains untouched.
86
from lp.services.scripts.tests import run_script
4953.7.10 by Stuart Bishop
Format imports
87
from lp.services.session.model import (
88
    SessionData,
89
    SessionPkgData,
90
    )
14578.2.1 by William Grant
Move librarian stuff from canonical.launchpad to lp.services.librarian. canonical.librarian remains untouched.
91
from lp.services.verification.interfaces.authtoken import LoginTokenType
92
from lp.services.verification.model.logintoken import LoginToken
14606.3.1 by William Grant
Merge canonical.database into lp.services.database.
93
from lp.services.webapp.interfaces import (
94
    IStoreSelector,
95
    MAIN_STORE,
96
    MASTER_FLAVOR,
97
    )
13635.1.1 by Ian Booth
Add garbo job to remove old answer contacts
98
from lp.services.worlddata.interfaces.language import ILanguageSet
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
99
from lp.testing import (
13635.1.1 by Ian Booth
Add garbo job to remove old answer contacts
100
    person_logged_in,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
101
    TestCase,
102
    TestCaseWithFactory,
103
    )
14606.3.1 by William Grant
Merge canonical.database into lp.services.database.
104
from lp.testing.layers import (
105
    DatabaseLayer,
106
    LaunchpadScriptLayer,
107
    LaunchpadZopelessLayer,
108
    ZopelessDatabaseLayer,
109
    )
13581.1.1 by Danilo Segan
Merge gmb's fix for 814576.
110
from lp.translations.model.potmsgset import POTMsgSet
111
from lp.translations.model.translationtemplateitem import (
112
    TranslationTemplateItem,
113
    )
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
114
115
116
class TestGarboScript(TestCase):
117
    layer = LaunchpadScriptLayer
118
119
    def test_daily_script(self):
120
        """Ensure garbo-daily.py actually runs."""
121
        rv, out, err = run_script(
122
            "cronscripts/garbo-daily.py", ["-q"], expect_returncode=0)
123
        self.failIf(out.strip(), "Output to stdout: %s" % out)
124
        self.failIf(err.strip(), "Output to stderr: %s" % err)
125
        DatabaseLayer.force_dirty_database()
126
127
    def test_hourly_script(self):
128
        """Ensure garbo-hourly.py actually runs."""
129
        rv, out, err = run_script(
130
            "cronscripts/garbo-hourly.py", ["-q"], expect_returncode=0)
131
        self.failIf(out.strip(), "Output to stdout: %s" % out)
132
        self.failIf(err.strip(), "Output to stderr: %s" % err)
133
134
8758.2.36 by Stuart Bishop
Don't use the Launchpad database to test the BulkPruner
135
class BulkFoo(Storm):
136
    __storm_table__ = 'bulkfoo'
137
    id = Int(primary=True)
138
139
140
class BulkFooPruner(BulkPruner):
141
    target_table_class = BulkFoo
142
    ids_to_prune_query = "SELECT id FROM BulkFoo WHERE id < 5"
8758.2.31 by Stuart Bishop
BulkPruner tests
143
    maximum_chunk_size = 2
144
145
146
class TestBulkPruner(TestCase):
8758.2.36 by Stuart Bishop
Don't use the Launchpad database to test the BulkPruner
147
    layer = ZopelessDatabaseLayer
8758.2.31 by Stuart Bishop
BulkPruner tests
148
8758.2.34 by Stuart Bishop
Improvements from review feedback
149
    def setUp(self):
150
        super(TestBulkPruner, self).setUp()
8758.2.36 by Stuart Bishop
Don't use the Launchpad database to test the BulkPruner
151
152
        self.store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
153
        self.store.execute("CREATE TABLE BulkFoo (id serial PRIMARY KEY)")
154
8758.2.34 by Stuart Bishop
Improvements from review feedback
155
        for i in range(10):
8758.2.36 by Stuart Bishop
Don't use the Launchpad database to test the BulkPruner
156
            self.store.add(BulkFoo())
8758.2.34 by Stuart Bishop
Improvements from review feedback
157
8758.2.63 by Stuart Bishop
Fix tests to cope with the more complex logging environment
158
        self.log = logging.getLogger('garbo')
159
8758.2.31 by Stuart Bishop
BulkPruner tests
160
    def test_bulkpruner(self):
8758.2.63 by Stuart Bishop
Fix tests to cope with the more complex logging environment
161
        pruner = BulkFooPruner(self.log)
8758.2.31 by Stuart Bishop
BulkPruner tests
162
8758.2.34 by Stuart Bishop
Improvements from review feedback
163
        # The loop thinks there is stuff to do. Confirm the initial
164
        # state is sane.
8758.2.31 by Stuart Bishop
BulkPruner tests
165
        self.assertFalse(pruner.isDone())
166
8758.2.34 by Stuart Bishop
Improvements from review feedback
167
        # An arbitrary chunk size.
168
        chunk_size = 2
169
8758.2.36 by Stuart Bishop
Don't use the Launchpad database to test the BulkPruner
170
        # Determine how many items to prune and to leave rather than
171
        # hardcode these numbers.
172
        num_to_prune = self.store.find(
173
            BulkFoo, BulkFoo.id < 5).count()
174
        num_to_leave = self.store.find(
175
            BulkFoo, BulkFoo.id >= 5).count()
8758.2.34 by Stuart Bishop
Improvements from review feedback
176
        self.assertTrue(num_to_prune > chunk_size)
8758.2.31 by Stuart Bishop
BulkPruner tests
177
        self.assertTrue(num_to_leave > 0)
178
8758.2.36 by Stuart Bishop
Don't use the Launchpad database to test the BulkPruner
179
        # Run one loop. Make sure it committed by throwing away
180
        # uncommitted changes.
8758.2.31 by Stuart Bishop
BulkPruner tests
181
        pruner(chunk_size)
182
        transaction.abort()
183
8758.2.34 by Stuart Bishop
Improvements from review feedback
184
        # Confirm 'chunk_size' items where removed; no more, no less.
8758.2.36 by Stuart Bishop
Don't use the Launchpad database to test the BulkPruner
185
        num_remaining = self.store.find(BulkFoo).count()
8758.2.31 by Stuart Bishop
BulkPruner tests
186
        expected_num_remaining = num_to_leave + num_to_prune - chunk_size
187
        self.assertEqual(num_remaining, expected_num_remaining)
188
8758.2.34 by Stuart Bishop
Improvements from review feedback
189
        # The loop thinks there is more stuff to do.
8758.2.31 by Stuart Bishop
BulkPruner tests
190
        self.assertFalse(pruner.isDone())
191
8758.2.34 by Stuart Bishop
Improvements from review feedback
192
        # Run the loop to completion, removing the remaining targetted
193
        # rows.
8758.2.31 by Stuart Bishop
BulkPruner tests
194
        while not pruner.isDone():
8758.2.34 by Stuart Bishop
Improvements from review feedback
195
            pruner(1000000)
8758.2.31 by Stuart Bishop
BulkPruner tests
196
        transaction.abort()
197
8758.2.34 by Stuart Bishop
Improvements from review feedback
198
        # Confirm we have removed all targetted rows.
8758.2.36 by Stuart Bishop
Don't use the Launchpad database to test the BulkPruner
199
        self.assertEqual(self.store.find(BulkFoo, BulkFoo.id < 5).count(), 0)
8758.2.34 by Stuart Bishop
Improvements from review feedback
200
201
        # Confirm we have the expected number of remaining rows.
202
        # With the previous check, this means no untargetted rows
203
        # where removed.
8758.2.31 by Stuart Bishop
BulkPruner tests
204
        self.assertEqual(
8758.2.36 by Stuart Bishop
Don't use the Launchpad database to test the BulkPruner
205
            self.store.find(BulkFoo, BulkFoo.id >= 5).count(), num_to_leave)
8758.2.31 by Stuart Bishop
BulkPruner tests
206
8758.2.35 by Stuart Bishop
Add optional cleanUp method to ITunableLoop
207
        # Cleanup clears up our resources.
208
        pruner.cleanUp()
209
8758.2.31 by Stuart Bishop
BulkPruner tests
210
        # We can run it again - temporary objects cleaned up.
8758.2.63 by Stuart Bishop
Fix tests to cope with the more complex logging environment
211
        pruner = BulkFooPruner(self.log)
8758.2.31 by Stuart Bishop
BulkPruner tests
212
        while not pruner.isDone():
213
            pruner(chunk_size)
214
215
4953.7.7 by Stuart Bishop
Session model classes and session pruner tests
216
class TestSessionPruner(TestCase):
217
    layer = ZopelessDatabaseLayer
218
219
    def setUp(self):
220
        super(TestCase, self).setUp()
4953.7.12 by Stuart Bishop
Use addCleanup instead of tearDown
221
222
        # Session database isn't reset between tests. We need to do this
223
        # manually.
224
        nuke_all_sessions = IMasterStore(SessionData).find(SessionData).remove
225
        nuke_all_sessions()
226
        self.addCleanup(nuke_all_sessions)
4953.7.7 by Stuart Bishop
Session model classes and session pruner tests
227
4953.7.11 by Stuart Bishop
datetime.now(UTC) is clearer
228
        recent = datetime.now(UTC)
4953.7.7 by Stuart Bishop
Session model classes and session pruner tests
229
        yesterday = recent - timedelta(days=1)
230
        ancient = recent - timedelta(days=61)
231
4953.7.17 by Stuart Bishop
Keep only last 6 authenticated sessions for a user
232
        self.make_session(u'recent_auth', recent, 'auth1')
233
        self.make_session(u'recent_unauth', recent, False)
234
        self.make_session(u'yesterday_auth', yesterday, 'auth2')
235
        self.make_session(u'yesterday_unauth', yesterday, False)
236
        self.make_session(u'ancient_auth', ancient, 'auth3')
237
        self.make_session(u'ancient_unauth', ancient, False)
238
8758.2.63 by Stuart Bishop
Fix tests to cope with the more complex logging environment
239
        self.log = logging.getLogger('garbo')
240
4953.7.17 by Stuart Bishop
Keep only last 6 authenticated sessions for a user
241
    def make_session(self, client_id, accessed, authenticated=None):
242
        session_data = SessionData()
243
        session_data.client_id = client_id
244
        session_data.last_accessed = accessed
245
        IMasterStore(SessionData).add(session_data)
246
247
        if authenticated:
248
            # Add login time information.
249
            session_pkg_data = SessionPkgData()
250
            session_pkg_data.client_id = client_id
251
            session_pkg_data.product_id = u'launchpad.authenticateduser'
252
            session_pkg_data.key = u'logintime'
253
            session_pkg_data.pickle = 'value is ignored'
254
            IMasterStore(SessionPkgData).add(session_pkg_data)
255
256
            # Add authenticated as information.
257
            session_pkg_data = SessionPkgData()
258
            session_pkg_data.client_id = client_id
259
            session_pkg_data.product_id = u'launchpad.authenticateduser'
260
            session_pkg_data.key = u'accountid'
261
            # Normally Account.id, but the session pruning works
262
            # at the SQL level and doesn't unpickle anything.
263
            session_pkg_data.pickle = authenticated
264
            IMasterStore(SessionPkgData).add(session_pkg_data)
4953.7.7 by Stuart Bishop
Session model classes and session pruner tests
265
266
    def sessionExists(self, client_id):
267
        store = IMasterStore(SessionData)
4953.7.8 by Stuart Bishop
Fix tests
268
        return not store.find(
269
            SessionData, SessionData.client_id == client_id).is_empty()
4953.7.7 by Stuart Bishop
Session model classes and session pruner tests
270
271
    def test_antique_session_pruner(self):
272
        chunk_size = 2
8758.2.63 by Stuart Bishop
Fix tests to cope with the more complex logging environment
273
        pruner = AntiqueSessionPruner(self.log)
4953.7.7 by Stuart Bishop
Session model classes and session pruner tests
274
        try:
275
            while not pruner.isDone():
276
                pruner(chunk_size)
277
        finally:
278
            pruner.cleanUp()
279
4953.7.8 by Stuart Bishop
Fix tests
280
        expected_sessions = set([
281
            u'recent_auth',
282
            u'recent_unauth',
283
            u'yesterday_auth',
284
            u'yesterday_unauth',
285
            # u'ancient_auth',
286
            # u'ancient_unauth',
287
            ])
288
289
        found_sessions = set(
290
            IMasterStore(SessionData).find(SessionData.client_id))
291
292
        self.assertEqual(expected_sessions, found_sessions)
4953.7.7 by Stuart Bishop
Session model classes and session pruner tests
293
294
    def test_unused_session_pruner(self):
295
        chunk_size = 2
8758.2.63 by Stuart Bishop
Fix tests to cope with the more complex logging environment
296
        pruner = UnusedSessionPruner(self.log)
4953.7.7 by Stuart Bishop
Session model classes and session pruner tests
297
        try:
298
            while not pruner.isDone():
299
                pruner(chunk_size)
300
        finally:
301
            pruner.cleanUp()
302
4953.7.8 by Stuart Bishop
Fix tests
303
        expected_sessions = set([
304
            u'recent_auth',
305
            u'recent_unauth',
306
            u'yesterday_auth',
307
            # u'yesterday_unauth',
308
            u'ancient_auth',
309
            # u'ancient_unauth',
310
            ])
311
312
        found_sessions = set(
313
            IMasterStore(SessionData).find(SessionData.client_id))
314
315
        self.assertEqual(expected_sessions, found_sessions)
4953.7.7 by Stuart Bishop
Session model classes and session pruner tests
316
4953.7.17 by Stuart Bishop
Keep only last 6 authenticated sessions for a user
317
    def test_duplicate_session_pruner(self):
318
        # None of the sessions created in setUp() are duplicates, so
319
        # they will all survive the pruning.
320
        expected_sessions = set([
321
            u'recent_auth',
322
            u'recent_unauth',
323
            u'yesterday_auth',
324
            u'yesterday_unauth',
325
            u'ancient_auth',
326
            u'ancient_unauth',
327
            ])
328
329
        now = datetime.now(UTC)
330
331
        # Make some duplicate logins from a few days ago.
332
        # Only the most recent 6 will be kept. Oldest is 'old dupe 9',
333
        # most recent 'old dupe 1'.
334
        for count in range(1, 10):
335
            self.make_session(
336
                u'old dupe %d' % count,
337
                now - timedelta(days=2, seconds=count),
338
                'old dupe')
339
        for count in range(1, 7):
340
            expected_sessions.add(u'old dupe %d' % count)
341
342
        # Make some other duplicate logins less than an hour old.
343
        # All of these will be kept.
344
        for count in range(1, 10):
345
            self.make_session(u'new dupe %d' % count, now, 'new dupe')
346
            expected_sessions.add(u'new dupe %d' % count)
347
348
        chunk_size = 2
8758.2.63 by Stuart Bishop
Fix tests to cope with the more complex logging environment
349
        pruner = DuplicateSessionPruner(self.log)
4953.7.17 by Stuart Bishop
Keep only last 6 authenticated sessions for a user
350
        try:
351
            while not pruner.isDone():
352
                pruner(chunk_size)
353
        finally:
354
            pruner.cleanUp()
355
356
        found_sessions = set(
357
            IMasterStore(SessionData).find(SessionData.client_id))
358
359
        self.assertEqual(expected_sessions, found_sessions)
360
4953.7.7 by Stuart Bishop
Session model classes and session pruner tests
361
8303.10.1 by James Henstridge
Garbo jobs to link people to RevisionAuthors and HWSubmissions as new
362
class TestGarbo(TestCaseWithFactory):
8758.2.38 by Stuart Bishop
Fix test layer
363
    layer = LaunchpadZopelessLayer
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
364
365
    def setUp(self):
366
        super(TestGarbo, self).setUp()
8758.2.63 by Stuart Bishop
Fix tests to cope with the more complex logging environment
367
368
        # Silence the root Logger by instructing the garbo logger to not
369
        # propagate messages.
370
        self.log = logging.getLogger('garbo')
371
        self.log.addHandler(NullHandler())
372
        self.log.propagate = 0
373
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
374
        # Run the garbage collectors to remove any existing garbage,
375
        # starting us in a known state.
376
        self.runDaily()
377
        self.runHourly()
8758.3.16 by Stuart Bishop
Fix tests for garbo-frequently.py jobs
378
        self.runFrequently()
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
379
8758.2.63 by Stuart Bishop
Fix tests to cope with the more complex logging environment
380
        # Capture garbo log output to tests can examine it.
381
        self.log_buffer = StringIO()
382
        handler = logging.StreamHandler(self.log_buffer)
383
        self.log.addHandler(handler)
384
8758.3.16 by Stuart Bishop
Fix tests for garbo-frequently.py jobs
385
    def runFrequently(self, maximum_chunk_size=2, test_args=()):
386
        transaction.commit()
387
        LaunchpadZopelessLayer.switchDbUser('garbo_daily')
388
        collector = FrequentDatabaseGarbageCollector(
389
            test_args=list(test_args))
390
        collector._maximum_chunk_size = maximum_chunk_size
391
        collector.logger = self.log
392
        collector.main()
393
        return collector
394
8758.2.9 by Stuart Bishop
Tests and fixes for BugNotificationPruner
395
    def runDaily(self, maximum_chunk_size=2, test_args=()):
7176.8.5 by Stuart Bishop
Fix transaction handling in garbo tests
396
        transaction.commit()
7675.177.6 by Stuart Bishop
Garbage collect OpenIDAssociations, allow scripts to correct to the auth store as the correct database user, ensure changing database connection settings in the test suite resets ZStorm so new credentials are used
397
        LaunchpadZopelessLayer.switchDbUser('garbo_daily')
8758.2.9 by Stuart Bishop
Tests and fixes for BugNotificationPruner
398
        collector = DailyDatabaseGarbageCollector(test_args=list(test_args))
7675.177.7 by Stuart Bishop
Ensure tests run multiple iterations of the LoopTuner and add logging
399
        collector._maximum_chunk_size = maximum_chunk_size
8758.2.63 by Stuart Bishop
Fix tests to cope with the more complex logging environment
400
        collector.logger = self.log
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
401
        collector.main()
8758.2.4 by Stuart Bishop
Tests to ensure PersonEmailAddressLinkChecker actually detects and reports corruption.
402
        return collector
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
403
8758.2.9 by Stuart Bishop
Tests and fixes for BugNotificationPruner
404
    def runHourly(self, maximum_chunk_size=2, test_args=()):
7675.177.6 by Stuart Bishop
Garbage collect OpenIDAssociations, allow scripts to correct to the auth store as the correct database user, ensure changing database connection settings in the test suite resets ZStorm so new credentials are used
405
        LaunchpadZopelessLayer.switchDbUser('garbo_hourly')
8758.2.9 by Stuart Bishop
Tests and fixes for BugNotificationPruner
406
        collector = HourlyDatabaseGarbageCollector(test_args=list(test_args))
7675.177.7 by Stuart Bishop
Ensure tests run multiple iterations of the LoopTuner and add logging
407
        collector._maximum_chunk_size = maximum_chunk_size
8758.2.63 by Stuart Bishop
Fix tests to cope with the more complex logging environment
408
        collector.logger = self.log
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
409
        collector.main()
8758.2.4 by Stuart Bishop
Tests to ensure PersonEmailAddressLinkChecker actually detects and reports corruption.
410
        return collector
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
411
412
    def test_OAuthNoncePruner(self):
4953.7.11 by Stuart Bishop
datetime.now(UTC) is clearer
413
        now = datetime.now(UTC)
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
414
        timestamps = [
13635.1.2 by Ian Booth
Lint
415
            now - timedelta(days=2),  # Garbage
416
            now - timedelta(days=1) - timedelta(seconds=60),  # Garbage
417
            now - timedelta(days=1) + timedelta(seconds=60),  # Not garbage
418
            now,  # Not garbage
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
419
            ]
420
        LaunchpadZopelessLayer.switchDbUser('testadmin')
7675.177.6 by Stuart Bishop
Garbage collect OpenIDAssociations, allow scripts to correct to the auth store as the correct database user, ensure changing database connection settings in the test suite resets ZStorm so new credentials are used
421
        store = IMasterStore(OAuthNonce)
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
422
423
        # Make sure we start with 0 nonces.
424
        self.failUnlessEqual(store.find(OAuthNonce).count(), 0)
425
426
        for timestamp in timestamps:
13365.3.3 by William Grant
Fix garbo to cope with new pk.
427
            store.add(OAuthNonce(
428
                access_token=OAuthAccessToken.get(1),
13635.1.2 by Ian Booth
Lint
429
                request_timestamp=timestamp,
430
                nonce=str(timestamp)))
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
431
        transaction.commit()
432
433
        # Make sure we have 4 nonces now.
434
        self.failUnlessEqual(store.find(OAuthNonce).count(), 4)
435
8758.3.16 by Stuart Bishop
Fix tests for garbo-frequently.py jobs
436
        self.runFrequently(
437
            maximum_chunk_size=60)  # 1 minute maximum chunk size
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
438
7675.177.6 by Stuart Bishop
Garbage collect OpenIDAssociations, allow scripts to correct to the auth store as the correct database user, ensure changing database connection settings in the test suite resets ZStorm so new credentials are used
439
        store = IMasterStore(OAuthNonce)
440
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
441
        # Now back to two, having removed the two garbage entries.
442
        self.failUnlessEqual(store.find(OAuthNonce).count(), 2)
443
444
        # And none of them are older than a day.
445
        # Hmm... why is it I'm putting tz aware datetimes in and getting
446
        # naive datetimes back? Bug in the SQLObject compatibility layer?
447
        # Test is still fine as we know the timezone.
448
        self.failUnless(
449
            store.find(
450
                Min(OAuthNonce.request_timestamp)).one().replace(tzinfo=UTC)
451
            >= now - timedelta(days=1))
452
7675.88.8 by Stuart Bishop
Stop ShipIt OpenID consumer sharing tables with the SSO server, dev replication setup fixes and test fixes
453
    def test_OpenIDConsumerNoncePruner(self):
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
454
        now = int(time.mktime(time.gmtime()))
455
        MINUTES = 60
456
        HOURS = 60 * 60
457
        DAYS = 24 * HOURS
458
        timestamps = [
13635.1.2 by Ian Booth
Lint
459
            now - 2 * DAYS,  # Garbage
460
            now - 1 * DAYS - 1 * MINUTES,  # Garbage
461
            now - 1 * DAYS + 1 * MINUTES,  # Not garbage
462
            now,  # Not garbage
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
463
            ]
464
        LaunchpadZopelessLayer.switchDbUser('testadmin')
465
7675.88.8 by Stuart Bishop
Stop ShipIt OpenID consumer sharing tables with the SSO server, dev replication setup fixes and test fixes
466
        store = IMasterStore(OpenIDConsumerNonce)
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
467
468
        # Make sure we start with 0 nonces.
7675.88.8 by Stuart Bishop
Stop ShipIt OpenID consumer sharing tables with the SSO server, dev replication setup fixes and test fixes
469
        self.failUnlessEqual(store.find(OpenIDConsumerNonce).count(), 0)
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
470
471
        for timestamp in timestamps:
8447.3.9 by James Henstridge
Fix test_OpenIDConsumerNoncePruner test.
472
            store.add(OpenIDConsumerNonce(
473
                    u'http://server/', timestamp, u'aa'))
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
474
        transaction.commit()
475
476
        # Make sure we have 4 nonces now.
7675.88.8 by Stuart Bishop
Stop ShipIt OpenID consumer sharing tables with the SSO server, dev replication setup fixes and test fixes
477
        self.failUnlessEqual(store.find(OpenIDConsumerNonce).count(), 4)
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
478
479
        # Run the garbage collector.
8758.3.16 by Stuart Bishop
Fix tests for garbo-frequently.py jobs
480
        self.runFrequently(maximum_chunk_size=60)  # 1 minute maximum chunks.
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
481
7675.177.6 by Stuart Bishop
Garbage collect OpenIDAssociations, allow scripts to correct to the auth store as the correct database user, ensure changing database connection settings in the test suite resets ZStorm so new credentials are used
482
        store = IMasterStore(OpenIDConsumerNonce)
483
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
484
        # We should now have 2 nonces.
7675.88.8 by Stuart Bishop
Stop ShipIt OpenID consumer sharing tables with the SSO server, dev replication setup fixes and test fixes
485
        self.failUnlessEqual(store.find(OpenIDConsumerNonce).count(), 2)
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
486
487
        # And none of them are older than 1 day
7675.88.8 by Stuart Bishop
Stop ShipIt OpenID consumer sharing tables with the SSO server, dev replication setup fixes and test fixes
488
        earliest = store.find(Min(OpenIDConsumerNonce.timestamp)).one()
13635.1.2 by Ian Booth
Lint
489
        self.failUnless(
490
            earliest >= now - 24 * 60 * 60, 'Still have old nonces')
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
491
492
    def test_CodeImportResultPruner(self):
4953.7.11 by Stuart Bishop
datetime.now(UTC) is clearer
493
        now = datetime.now(UTC)
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
494
        store = IMasterStore(CodeImportResult)
495
8377.9.10 by Michael Hudson
untested introduction of a config value for failure limit
496
        results_to_keep_count = (
497
            config.codeimport.consecutive_failure_limit - 1)
498
7675.743.4 by Jonathan Lange
Don't use sample data in garbo test for codeimport
499
        LaunchpadZopelessLayer.switchDbUser('testadmin')
500
        code_import_id = self.factory.makeCodeImport().id
501
        machine_id = self.factory.makeCodeImportMachine().id
502
        requester_id = self.factory.makePerson().id
503
        transaction.commit()
504
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
505
        def new_code_import_result(timestamp):
506
            LaunchpadZopelessLayer.switchDbUser('testadmin')
507
            CodeImportResult(
508
                date_created=timestamp,
7675.743.4 by Jonathan Lange
Don't use sample data in garbo test for codeimport
509
                code_importID=code_import_id, machineID=machine_id,
510
                requesting_userID=requester_id,
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
511
                status=CodeImportResultStatus.FAILURE,
512
                date_job_started=timestamp)
513
            transaction.commit()
514
515
        new_code_import_result(now - timedelta(days=60))
8377.9.10 by Michael Hudson
untested introduction of a config value for failure limit
516
        for i in range(results_to_keep_count - 1):
13635.1.2 by Ian Booth
Lint
517
            new_code_import_result(now - timedelta(days=19 + i))
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
518
519
        # Run the garbage collector
520
        self.runDaily()
521
8377.9.10 by Michael Hudson
untested introduction of a config value for failure limit
522
        # Nothing is removed, because we always keep the
523
        # ``results_to_keep_count`` latest.
7675.177.6 by Stuart Bishop
Garbage collect OpenIDAssociations, allow scripts to correct to the auth store as the correct database user, ensure changing database connection settings in the test suite resets ZStorm so new credentials are used
524
        store = IMasterStore(CodeImportResult)
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
525
        self.failUnlessEqual(
8377.9.10 by Michael Hudson
untested introduction of a config value for failure limit
526
            results_to_keep_count,
527
            store.find(CodeImportResult).count())
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
528
529
        new_code_import_result(now - timedelta(days=31))
530
        self.runDaily()
7675.177.6 by Stuart Bishop
Garbage collect OpenIDAssociations, allow scripts to correct to the auth store as the correct database user, ensure changing database connection settings in the test suite resets ZStorm so new credentials are used
531
        store = IMasterStore(CodeImportResult)
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
532
        self.failUnlessEqual(
8377.9.10 by Michael Hudson
untested introduction of a config value for failure limit
533
            results_to_keep_count,
534
            store.find(CodeImportResult).count())
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
535
536
        new_code_import_result(now - timedelta(days=29))
537
        self.runDaily()
7675.177.6 by Stuart Bishop
Garbage collect OpenIDAssociations, allow scripts to correct to the auth store as the correct database user, ensure changing database connection settings in the test suite resets ZStorm so new credentials are used
538
        store = IMasterStore(CodeImportResult)
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
539
        self.failUnlessEqual(
8377.9.10 by Michael Hudson
untested introduction of a config value for failure limit
540
            results_to_keep_count,
541
            store.find(CodeImportResult).count())
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
542
543
        # We now have no CodeImportResults older than 30 days
544
        self.failUnless(
545
            store.find(
546
                Min(CodeImportResult.date_created)).one().replace(tzinfo=UTC)
547
            >= now - timedelta(days=30))
548
11703.1.4 by Tim Penhey
Prune the CodeImportEvents.
549
    def test_CodeImportEventPruner(self):
4953.7.11 by Stuart Bishop
datetime.now(UTC) is clearer
550
        now = datetime.now(UTC)
11703.1.4 by Tim Penhey
Prune the CodeImportEvents.
551
        store = IMasterStore(CodeImportResult)
552
553
        LaunchpadZopelessLayer.switchDbUser('testadmin')
554
        machine = self.factory.makeCodeImportMachine()
555
        requester = self.factory.makePerson()
556
        # Create 6 code import events for this machine, 3 on each side of 30
11768.1.3 by Curtis Hovey
Hushed lint
557
        # days. Use the event set to the extra event data rows get created
558
        # too.
11703.1.4 by Tim Penhey
Prune the CodeImportEvents.
559
        event_set = getUtility(ICodeImportEventSet)
560
        for age in (35, 33, 31, 29, 27, 15):
561
            event_set.newOnline(
562
                machine, user=requester, message='Hello',
563
                _date_created=(now - timedelta(days=age)))
564
        transaction.commit()
565
566
        # Run the garbage collector
567
        self.runDaily()
568
569
        # Only the three most recent results are left.
570
        events = list(machine.events)
571
        self.assertEqual(3, len(events))
572
        # We now have no CodeImportEvents older than 30 days
573
        self.failUnless(
574
            store.find(
575
                Min(CodeImportEvent.date_created)).one().replace(tzinfo=UTC)
576
            >= now - timedelta(days=30))
577
10556.4.3 by Guilherme Salgado
Remove the auth_* attributes from DatabaseConfig and fix a bunch of tests which used to rely on auth stores.
578
    def test_OpenIDConsumerAssociationPruner(self):
579
        pruner = OpenIDConsumerAssociationPruner
7675.177.6 by Stuart Bishop
Garbage collect OpenIDAssociations, allow scripts to correct to the auth store as the correct database user, ensure changing database connection settings in the test suite resets ZStorm so new credentials are used
580
        table_name = pruner.table_name
581
        LaunchpadZopelessLayer.switchDbUser('testadmin')
582
        store_selector = getUtility(IStoreSelector)
10556.4.3 by Guilherme Salgado
Remove the auth_* attributes from DatabaseConfig and fix a bunch of tests which used to rely on auth stores.
583
        store = store_selector.get(MAIN_STORE, MASTER_FLAVOR)
7675.177.6 by Stuart Bishop
Garbage collect OpenIDAssociations, allow scripts to correct to the auth store as the correct database user, ensure changing database connection settings in the test suite resets ZStorm so new credentials are used
584
        now = time.time()
585
        # Create some associations in the past with lifetimes
586
        for delta in range(0, 20):
587
            store.execute("""
588
                INSERT INTO %s (server_url, handle, issued, lifetime)
589
                VALUES (%s, %s, %d, %d)
13635.1.2 by Ian Booth
Lint
590
                """ % (table_name, str(delta), str(delta), now - 10, delta))
7675.177.6 by Stuart Bishop
Garbage collect OpenIDAssociations, allow scripts to correct to the auth store as the correct database user, ensure changing database connection settings in the test suite resets ZStorm so new credentials are used
591
        transaction.commit()
592
7675.177.9 by Stuart Bishop
Comment OpenIDAssociationPruner test and confirm it isn't trashing everything
593
        # Ensure that we created at least one expirable row (using the
594
        # test start time as 'now').
7675.177.6 by Stuart Bishop
Garbage collect OpenIDAssociations, allow scripts to correct to the auth store as the correct database user, ensure changing database connection settings in the test suite resets ZStorm so new credentials are used
595
        num_expired = store.execute("""
596
            SELECT COUNT(*) FROM %s
597
            WHERE issued + lifetime < %f
598
            """ % (table_name, now)).get_one()[0]
599
        self.failUnless(num_expired > 0)
600
7675.177.9 by Stuart Bishop
Comment OpenIDAssociationPruner test and confirm it isn't trashing everything
601
        # Expire all those expirable rows, and possibly a few more if this
602
        # test is running slow.
8758.3.16 by Stuart Bishop
Fix tests for garbo-frequently.py jobs
603
        self.runFrequently()
7675.177.6 by Stuart Bishop
Garbage collect OpenIDAssociations, allow scripts to correct to the auth store as the correct database user, ensure changing database connection settings in the test suite resets ZStorm so new credentials are used
604
605
        LaunchpadZopelessLayer.switchDbUser('testadmin')
10556.4.3 by Guilherme Salgado
Remove the auth_* attributes from DatabaseConfig and fix a bunch of tests which used to rely on auth stores.
606
        store = store_selector.get(MAIN_STORE, MASTER_FLAVOR)
7675.177.9 by Stuart Bishop
Comment OpenIDAssociationPruner test and confirm it isn't trashing everything
607
        # Confirm all the rows we know should have been expired have
608
        # been expired. These are the ones that would be expired using
609
        # the test start time as 'now'.
7675.177.6 by Stuart Bishop
Garbage collect OpenIDAssociations, allow scripts to correct to the auth store as the correct database user, ensure changing database connection settings in the test suite resets ZStorm so new credentials are used
610
        num_expired = store.execute("""
611
            SELECT COUNT(*) FROM %s
612
            WHERE issued + lifetime < %f
613
            """ % (table_name, now)).get_one()[0]
614
        self.failUnlessEqual(num_expired, 0)
615
7675.177.9 by Stuart Bishop
Comment OpenIDAssociationPruner test and confirm it isn't trashing everything
616
        # Confirm that we haven't expired everything. This test will fail
617
        # if it has taken 10 seconds to get this far.
618
        num_unexpired = store.execute(
619
            "SELECT COUNT(*) FROM %s" % table_name).get_one()[0]
620
        self.failUnless(num_unexpired > 0)
621
8303.10.5 by James Henstridge
Update tests that expect RevisionAuthors or HWSubmissions to be linked
622
    def test_RevisionAuthorEmailLinker(self):
8303.10.1 by James Henstridge
Garbo jobs to link people to RevisionAuthors and HWSubmissions as new
623
        LaunchpadZopelessLayer.switchDbUser('testadmin')
624
        rev1 = self.factory.makeRevision('Author 1 <author-1@Example.Org>')
625
        rev2 = self.factory.makeRevision('Author 2 <author-2@Example.Org>')
626
627
        person1 = self.factory.makePerson(email='Author-1@example.org')
628
        person2 = self.factory.makePerson(
629
            email='Author-2@example.org',
630
            email_address_status=EmailAddressStatus.NEW)
631
632
        self.assertEqual(rev1.revision_author.person, None)
633
        self.assertEqual(rev2.revision_author.person, None)
634
635
        self.runDaily()
636
637
        # Only the validated email address associated with a Person
638
        # causes a linkage.
639
        LaunchpadZopelessLayer.switchDbUser('testadmin')
640
        self.assertEqual(rev1.revision_author.person, person1)
641
        self.assertEqual(rev2.revision_author.person, None)
642
643
        # Validating an email address creates a linkage.
644
        person2.validateAndEnsurePreferredEmail(person2.guessedemails[0])
645
        self.assertEqual(rev2.revision_author.person, None)
646
647
        self.runDaily()
648
        LaunchpadZopelessLayer.switchDbUser('testadmin')
649
        self.assertEqual(rev2.revision_author.person, person2)
650
8303.10.5 by James Henstridge
Update tests that expect RevisionAuthors or HWSubmissions to be linked
651
    def test_HWSubmissionEmailLinker(self):
8303.10.1 by James Henstridge
Garbo jobs to link people to RevisionAuthors and HWSubmissions as new
652
        LaunchpadZopelessLayer.switchDbUser('testadmin')
653
        sub1 = self.factory.makeHWSubmission(
654
            emailaddress='author-1@Example.Org')
655
        sub2 = self.factory.makeHWSubmission(
656
            emailaddress='author-2@Example.Org')
657
658
        person1 = self.factory.makePerson(email='Author-1@example.org')
659
        person2 = self.factory.makePerson(
660
            email='Author-2@example.org',
661
            email_address_status=EmailAddressStatus.NEW)
662
663
        self.assertEqual(sub1.owner, None)
664
        self.assertEqual(sub2.owner, None)
665
666
        self.runDaily()
667
668
        # Only the validated email address associated with a Person
669
        # causes a linkage.
670
        LaunchpadZopelessLayer.switchDbUser('testadmin')
671
        self.assertEqual(sub1.owner, person1)
672
        self.assertEqual(sub2.owner, None)
673
674
        # Validating an email address creates a linkage.
675
        person2.validateAndEnsurePreferredEmail(person2.guessedemails[0])
676
        self.assertEqual(sub2.owner, None)
677
678
        self.runDaily()
679
        LaunchpadZopelessLayer.switchDbUser('testadmin')
680
        self.assertEqual(sub2.owner, person2)
681
8697.25.5 by Stuart Bishop
Add --experimental option to garbo-*.py, and move PersonPruner to the experimental list so it is only run on staging
682
    def test_PersonPruner(self):
8697.25.1 by Guilherme Salgado
New task ran as part of garbo-daily to delete unlinked person entries.
683
        personset = getUtility(IPersonSet)
684
        # Switch the DB user because the garbo_daily user isn't allowed to
685
        # create person entries.
686
        LaunchpadZopelessLayer.switchDbUser('testadmin')
687
688
        # Create two new person entries, both not linked to anything. One of
689
        # them will have the present day as its date created, and so will not
690
        # be deleted, whereas the other will have a creation date far in the
691
        # past, so it will be deleted.
12521.5.3 by Steve Kowalik
More lint.
692
        self.factory.makePerson(name='test-unlinked-person-new')
8697.25.1 by Guilherme Salgado
New task ran as part of garbo-daily to delete unlinked person entries.
693
        person_old = self.factory.makePerson(name='test-unlinked-person-old')
694
        removeSecurityProxy(person_old).datecreated = datetime(
695
            2008, 01, 01, tzinfo=UTC)
696
8697.25.5 by Stuart Bishop
Add --experimental option to garbo-*.py, and move PersonPruner to the experimental list so it is only run on staging
697
        # Normally, the garbage collector will do nothing because the
698
        # PersonPruner is experimental
8697.25.1 by Guilherme Salgado
New task ran as part of garbo-daily to delete unlinked person entries.
699
        self.runDaily()
8697.25.5 by Stuart Bishop
Add --experimental option to garbo-*.py, and move PersonPruner to the experimental list so it is only run on staging
700
        self.assertIsNot(
701
            personset.getByName('test-unlinked-person-new'), None)
702
        self.assertIsNot(
703
            personset.getByName('test-unlinked-person-old'), None)
8697.25.1 by Guilherme Salgado
New task ran as part of garbo-daily to delete unlinked person entries.
704
8697.25.5 by Stuart Bishop
Add --experimental option to garbo-*.py, and move PersonPruner to the experimental list so it is only run on staging
705
        # When we run the garbage collector with experimental jobs turned
706
        # on, the old unlinked Person is removed.
707
        self.runDaily(test_args=['--experimental'])
8697.25.1 by Guilherme Salgado
New task ran as part of garbo-daily to delete unlinked person entries.
708
        self.assertIsNot(
709
            personset.getByName('test-unlinked-person-new'), None)
710
        self.assertIs(personset.getByName('test-unlinked-person-old'), None)
711
8758.2.9 by Stuart Bishop
Tests and fixes for BugNotificationPruner
712
    def test_BugNotificationPruner(self):
713
        # Create some sample data
714
        LaunchpadZopelessLayer.switchDbUser('testadmin')
715
        notification = BugNotification(
716
            messageID=1,
717
            bugID=1,
718
            is_comment=True,
719
            date_emailed=None)
12521.5.3 by Steve Kowalik
More lint.
720
        BugNotificationRecipient(
8758.2.9 by Stuart Bishop
Tests and fixes for BugNotificationPruner
721
            bug_notification=notification,
722
            personID=1,
723
            reason_header='Whatever',
724
            reason_body='Whatever')
725
        # We don't create an entry exactly 30 days old to avoid
726
        # races in the test.
727
        for delta in range(-45, -14, 2):
728
            message = Message(rfc822msgid=str(delta))
729
            notification = BugNotification(
730
                message=message,
731
                bugID=1,
732
                is_comment=True,
733
                date_emailed=UTC_NOW + SQL("interval '%d days'" % delta))
12521.5.3 by Steve Kowalik
More lint.
734
            BugNotificationRecipient(
8758.2.9 by Stuart Bishop
Tests and fixes for BugNotificationPruner
735
                bug_notification=notification,
736
                personID=1,
737
                reason_header='Whatever',
738
                reason_body='Whatever')
739
740
        store = IMasterStore(BugNotification)
741
742
        # Ensure we are at a known starting point.
743
        num_unsent = store.find(
744
            BugNotification,
745
            BugNotification.date_emailed == None).count()
746
        num_old = store.find(
747
            BugNotification,
748
            BugNotification.date_emailed < THIRTY_DAYS_AGO).count()
749
        num_new = store.find(
750
            BugNotification,
751
            BugNotification.date_emailed > THIRTY_DAYS_AGO).count()
752
753
        self.assertEqual(num_unsent, 1)
754
        self.assertEqual(num_old, 8)
755
        self.assertEqual(num_new, 8)
756
757
        # Run the garbage collector.
758
        self.runDaily()
759
760
        # We should have 9 BugNotifications left.
761
        self.assertEqual(
762
            store.find(
763
                BugNotification,
764
                BugNotification.date_emailed == None).count(),
765
            num_unsent)
766
        self.assertEqual(
767
            store.find(
768
                BugNotification,
769
                BugNotification.date_emailed > THIRTY_DAYS_AGO).count(),
770
            num_new)
771
        self.assertEqual(
772
            store.find(
773
                BugNotification,
774
                BugNotification.date_emailed < THIRTY_DAYS_AGO).count(),
775
            0)
776
13635.1.1 by Ian Booth
Add garbo job to remove old answer contacts
777
    def _test_AnswerContactPruner(self, status, interval, expected_count=0):
778
        # Garbo should remove answer contacts for accounts with given 'status'
779
        # which was set more than 'interval' days ago.
780
        LaunchpadZopelessLayer.switchDbUser('testadmin')
781
        store = IMasterStore(AnswerContact)
782
783
        person = self.factory.makePerson()
784
        person.addLanguage(getUtility(ILanguageSet)['en'])
785
        question = self.factory.makeQuestion()
786
        with person_logged_in(question.owner):
787
            question.target.addAnswerContact(person, person)
788
        Store.of(question).flush()
789
        self.assertEqual(
790
            store.find(
791
                AnswerContact,
792
                AnswerContact.person == person.id).count(),
793
                1)
794
795
        account = person.account
796
        account.status = status
797
        # We flush because a trigger sets the date_status_set and we need to
798
        # modify it ourselves.
799
        Store.of(account).flush()
800
        if interval is not None:
801
            account.date_status_set = interval
802
803
        self.runDaily()
804
805
        LaunchpadZopelessLayer.switchDbUser('testadmin')
806
        self.assertEqual(
807
            store.find(
808
                AnswerContact,
809
                AnswerContact.person == person.id).count(),
810
                expected_count)
811
812
    def test_AnswerContactPruner_deactivated_accounts(self):
813
        # Answer contacts with an account deactivated at least one day ago
814
        # should be pruned.
815
        self._test_AnswerContactPruner(AccountStatus.DEACTIVATED, ONE_DAY_AGO)
816
817
    def test_AnswerContactPruner_suspended_accounts(self):
818
        # Answer contacts with an account suspended at least seven days ago
819
        # should be pruned.
820
        self._test_AnswerContactPruner(
821
            AccountStatus.SUSPENDED, SEVEN_DAYS_AGO)
822
823
    def test_AnswerContactPruner_doesnt_prune_recently_changed_accounts(self):
824
        # Answer contacts which are suspended or deactivated inside the
825
        # minimum time interval are not pruned.
826
        self._test_AnswerContactPruner(
827
            AccountStatus.DEACTIVATED, None, expected_count=1)
828
        self._test_AnswerContactPruner(
829
            AccountStatus.SUSPENDED, ONE_DAY_AGO, expected_count=1)
14039.1.8 by Brad Crittenden
Fixed lint
830
7675.440.8 by Paul Hummer
Turned the job pruner into a branch job pruner
831
    def test_BranchJobPruner(self):
7675.440.12 by Paul Hummer
Added comments, removed tempfile.
832
        # Garbo should remove jobs completed over 30 days ago.
7675.440.1 by Paul Hummer
Added JobPruner to garbo daily with accompanying tests
833
        LaunchpadZopelessLayer.switchDbUser('testadmin')
7675.440.2 by Paul Hummer
Added failing test for JobPruner
834
        store = IMasterStore(Job)
835
7675.440.10 by Paul Hummer
Removed creating a tree
836
        db_branch = self.factory.makeAnyBranch()
7675.440.2 by Paul Hummer
Added failing test for JobPruner
837
        db_branch.branch_format = BranchFormat.BZR_BRANCH_5
838
        db_branch.repository_format = RepositoryFormat.BZR_KNIT_1
7176.8.7 by Stuart Bishop
Just a flush needed
839
        Store.of(db_branch).flush()
13717.1.3 by Aaron Bentley
Include requester in BranchUpgradeJobs.
840
        branch_job = BranchUpgradeJob.create(
841
            db_branch, self.factory.makePerson())
7675.440.3 by Paul Hummer
Committing at a WTF moment to go take a walk
842
        branch_job.job.date_finished = THIRTY_DAYS_AGO
7675.440.4 by Paul Hummer
Got a working test
843
844
        self.assertEqual(
845
            store.find(
846
                BranchJob,
847
                BranchJob.branch == db_branch.id).count(),
848
                1)
7675.440.2 by Paul Hummer
Added failing test for JobPruner
849
12521.5.3 by Steve Kowalik
More lint.
850
        self.runDaily()
7675.440.2 by Paul Hummer
Added failing test for JobPruner
851
7675.440.4 by Paul Hummer
Got a working test
852
        LaunchpadZopelessLayer.switchDbUser('testadmin')
7675.440.2 by Paul Hummer
Added failing test for JobPruner
853
        self.assertEqual(
7675.440.4 by Paul Hummer
Got a working test
854
            store.find(
855
                BranchJob,
856
                BranchJob.branch == db_branch.id).count(),
857
                0)
7675.440.1 by Paul Hummer
Added JobPruner to garbo daily with accompanying tests
858
7675.440.9 by Paul Hummer
Added another test to make sure that garbo only deletes completed branch jobs that are old.
859
    def test_BranchJobPruner_doesnt_prune_recent_jobs(self):
7675.440.12 by Paul Hummer
Added comments, removed tempfile.
860
        # Check to make sure the garbo doesn't remove jobs that aren't more
861
        # than thirty days old.
7675.440.9 by Paul Hummer
Added another test to make sure that garbo only deletes completed branch jobs that are old.
862
        LaunchpadZopelessLayer.switchDbUser('testadmin')
863
        store = IMasterStore(Job)
864
7675.477.1 by Paul Hummer
Reverted the reversion of the patch that went into devel by mistake
865
        db_branch = self.factory.makeAnyBranch(
866
            branch_format=BranchFormat.BZR_BRANCH_5,
867
            repository_format=RepositoryFormat.BZR_KNIT_1)
7675.440.9 by Paul Hummer
Added another test to make sure that garbo only deletes completed branch jobs that are old.
868
13717.1.3 by Aaron Bentley
Include requester in BranchUpgradeJobs.
869
        branch_job = BranchUpgradeJob.create(
870
            db_branch, self.factory.makePerson())
7675.440.9 by Paul Hummer
Added another test to make sure that garbo only deletes completed branch jobs that are old.
871
        branch_job.job.date_finished = THIRTY_DAYS_AGO
872
7675.477.1 by Paul Hummer
Reverted the reversion of the patch that went into devel by mistake
873
        db_branch2 = self.factory.makeAnyBranch(
874
            branch_format=BranchFormat.BZR_BRANCH_5,
875
            repository_format=RepositoryFormat.BZR_KNIT_1)
13717.1.3 by Aaron Bentley
Include requester in BranchUpgradeJobs.
876
        BranchUpgradeJob.create(db_branch2, self.factory.makePerson())
7675.440.9 by Paul Hummer
Added another test to make sure that garbo only deletes completed branch jobs that are old.
877
12521.5.3 by Steve Kowalik
More lint.
878
        self.runDaily()
7675.440.9 by Paul Hummer
Added another test to make sure that garbo only deletes completed branch jobs that are old.
879
880
        LaunchpadZopelessLayer.switchDbUser('testadmin')
12521.5.3 by Steve Kowalik
More lint.
881
        self.assertEqual(store.find(BranchJob).count(), 1)
7675.440.9 by Paul Hummer
Added another test to make sure that garbo only deletes completed branch jobs that are old.
882
8758.2.47 by Stuart Bishop
Switch ObsoleteBugAttachmentPruner to use BulkPruner
883
    def test_ObsoleteBugAttachmentPruner(self):
10606.5.2 by Abel Deuring
new garbo job: delete bug attachments that don't have a LibraryFileContent record
884
        # Bug attachments without a LibraryFileContent record are removed.
885
886
        LaunchpadZopelessLayer.switchDbUser('testadmin')
887
        bug = self.factory.makeBug()
888
        attachment = self.factory.makeBugAttachment(bug=bug)
889
        transaction.commit()
890
891
        # Bug attachments that have a LibraryFileContent record are
892
        # not deleted.
893
        self.assertIsNot(attachment.libraryfile.content, None)
894
        self.runDaily()
895
        self.assertEqual(bug.attachments.count(), 1)
896
897
        # But once we delete the LfC record, the attachment is deleted
898
        # in the next daily garbo run.
899
        LaunchpadZopelessLayer.switchDbUser('testadmin')
900
        removeSecurityProxy(attachment.libraryfile).content = None
901
        transaction.commit()
902
        self.runDaily()
903
        LaunchpadZopelessLayer.switchDbUser('testadmin')
904
        self.assertEqual(bug.attachments.count(), 0)
905
7675.809.6 by Robert Collins
Create a garbo daily task to clean up file access tokens.
906
    def test_TimeLimitedTokenPruner(self):
907
        # Ensure there are no tokens
908
        store = sqlbase.session_store()
909
        map(store.remove, store.find(TimeLimitedToken))
910
        store.flush()
911
        self.assertEqual(0, len(list(store.find(TimeLimitedToken,
7675.809.30 by Robert Collins
Rename url to path in TimeLimitedToken.
912
            path="sample path"))))
7675.809.8 by Robert Collins
Test that only old file access tokens are garbage collected.
913
        # One to clean and one to keep
7675.809.30 by Robert Collins
Rename url to path in TimeLimitedToken.
914
        store.add(TimeLimitedToken(path="sample path", token="foo",
7675.809.6 by Robert Collins
Create a garbo daily task to clean up file access tokens.
915
            created=datetime(2008, 01, 01, tzinfo=UTC)))
7675.809.30 by Robert Collins
Rename url to path in TimeLimitedToken.
916
        store.add(TimeLimitedToken(path="sample path", token="bar")),
7675.809.6 by Robert Collins
Create a garbo daily task to clean up file access tokens.
917
        store.commit()
7675.809.8 by Robert Collins
Test that only old file access tokens are garbage collected.
918
        self.assertEqual(2, len(list(store.find(TimeLimitedToken,
7675.809.30 by Robert Collins
Rename url to path in TimeLimitedToken.
919
            path="sample path"))))
7675.809.8 by Robert Collins
Test that only old file access tokens are garbage collected.
920
        self.runDaily()
921
        self.assertEqual(0, len(list(store.find(TimeLimitedToken,
7675.809.30 by Robert Collins
Rename url to path in TimeLimitedToken.
922
            path="sample path", token="foo"))))
7675.809.6 by Robert Collins
Create a garbo daily task to clean up file access tokens.
923
        self.assertEqual(1, len(list(store.find(TimeLimitedToken,
7675.809.30 by Robert Collins
Rename url to path in TimeLimitedToken.
924
            path="sample path", token="bar"))))
7675.809.14 by Robert Collins
Merge trunk resolving conflicts, fingers crossed.
925
7675.758.8 by Jeroen Vermeulen
Okay, okay, running it through garbo instead of cron.
926
    def test_CacheSuggestivePOTemplates(self):
927
        LaunchpadZopelessLayer.switchDbUser('testadmin')
928
        template = self.factory.makePOTemplate()
929
        self.runDaily()
930
931
        store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
932
        count, = store.execute("""
933
            SELECT count(*)
934
            FROM SuggestivePOTemplate
935
            WHERE potemplate = %s
7675.809.14 by Robert Collins
Merge trunk resolving conflicts, fingers crossed.
936
            """ % sqlbase.quote(template.id)).get_one()
7675.758.8 by Jeroen Vermeulen
Okay, okay, running it through garbo instead of cron.
937
938
        self.assertEqual(1, count)
8758.3.20 by Stuart Bishop
Add garbo job to rollup BugSummaryJournal
939
940
    def test_BugSummaryJournalRollup(self):
941
        LaunchpadZopelessLayer.switchDbUser('testadmin')
942
        store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
943
944
        # Generate a load of entries in BugSummaryJournal.
945
        store.execute("UPDATE BugTask SET status=42")
946
947
        # We only need a few to test.
948
        num_rows = store.execute(
949
            "SELECT COUNT(*) FROM BugSummaryJournal").get_one()[0]
950
        self.assertThat(num_rows, GreaterThan(10))
951
952
        self.runFrequently()
953
954
        # We just care that the rows have been removed. The bugsummary
955
        # tests confirm that the rollup stored method is working correctly.
956
        num_rows = store.execute(
957
            "SELECT COUNT(*) FROM BugSummaryJournal").get_one()[0]
958
        self.assertThat(num_rows, Equals(0))
13581.1.1 by Danilo Segan
Merge gmb's fix for 814576.
959
960
    def test_UnusedPOTMsgSetPruner_removes_obsolete_message_sets(self):
961
        # UnusedPOTMsgSetPruner removes any POTMsgSet that are
962
        # participating in a POTemplate only as obsolete messages.
963
        LaunchpadZopelessLayer.switchDbUser('testadmin')
964
        pofile = self.factory.makePOFile()
965
        translation_message = self.factory.makeCurrentTranslationMessage(
966
            pofile=pofile)
967
        translation_message.potmsgset.setSequence(
968
            pofile.potemplate, 0)
969
        transaction.commit()
970
        store = IMasterStore(POTMsgSet)
971
        obsolete_msgsets = store.find(
972
            POTMsgSet,
973
            TranslationTemplateItem.potmsgset == POTMsgSet.id,
974
            TranslationTemplateItem.sequence == 0)
975
        self.assertNotEqual(0, obsolete_msgsets.count())
976
        self.runDaily()
977
        self.assertEqual(0, obsolete_msgsets.count())
978
979
    def test_UnusedPOTMsgSetPruner_removes_unreferenced_message_sets(self):
980
        # If a POTMsgSet is not referenced by any templates the
981
        # UnusedPOTMsgSetPruner will remove it.
982
        LaunchpadZopelessLayer.switchDbUser('testadmin')
983
        potmsgset = self.factory.makePOTMsgSet()
984
        # Cheekily drop any references to the POTMsgSet we just created.
985
        store = IMasterStore(POTMsgSet)
986
        store.execute(
987
            "DELETE FROM TranslationTemplateItem WHERE potmsgset = %s"
988
            % potmsgset.id)
989
        transaction.commit()
990
        unreferenced_msgsets = store.find(
991
            POTMsgSet,
992
            Not(In(
993
                POTMsgSet.id,
994
                SQL("SELECT potmsgset FROM TranslationTemplateItem"))))
995
        self.assertNotEqual(0, unreferenced_msgsets.count())
996
        self.runDaily()
997
        self.assertEqual(0, unreferenced_msgsets.count())
13646.11.17 by Steve Kowalik
Write tests for the two new populators
998
14625.2.2 by Colin Watson
Add garbo job to clean up broken SPR.dsc_binaries values.
999
    def test_SourcePackageReleaseDscBinariesUpdater_updates_incorrect(self):
1000
        # SourcePackageReleaseDscBinariesUpdater fixes incorrectly-separated
1001
        # dsc_binaries values.
1002
        LaunchpadZopelessLayer.switchDbUser('testadmin')
14625.2.5 by Colin Watson
Simplify test as suggested by Gavin Panella.
1003
        spr = self.factory.makeSourcePackageRelease(
1004
            dsc_binaries="one two three")
14625.2.2 by Colin Watson
Add garbo job to clean up broken SPR.dsc_binaries values.
1005
        transaction.commit()
1006
        self.runDaily()
14625.2.5 by Colin Watson
Simplify test as suggested by Gavin Panella.
1007
        self.assertEqual("one, two, three", spr.dsc_binaries)
14625.2.2 by Colin Watson
Add garbo job to clean up broken SPR.dsc_binaries values.
1008
1009
    def test_SourcePackageReleaseDscBinariesUpdater_skips_correct(self):
1010
        # SourcePackageReleaseDscBinariesUpdater leaves correct dsc_binaries
1011
        # values alone.
1012
        LaunchpadZopelessLayer.switchDbUser('testadmin')
14625.2.5 by Colin Watson
Simplify test as suggested by Gavin Panella.
1013
        spr_one = self.factory.makeSourcePackageRelease(dsc_binaries="one")
1014
        spr_three = self.factory.makeSourcePackageRelease(
1015
            dsc_binaries="one, two, three")
14625.2.2 by Colin Watson
Add garbo job to clean up broken SPR.dsc_binaries values.
1016
        transaction.commit()
1017
        self.runDaily()
14625.2.5 by Colin Watson
Simplify test as suggested by Gavin Panella.
1018
        self.assertEqual("one", spr_one.dsc_binaries)
1019
        self.assertEqual("one, two, three", spr_three.dsc_binaries)
14625.2.2 by Colin Watson
Add garbo job to clean up broken SPR.dsc_binaries values.
1020
1021
    def test_SourcePackageReleaseDscBinariesUpdater_skips_broken(self):
1022
        # There have been a few instances of Binary fields in PPA packages
1023
        # that are formatted like a dependency relationship field, complete
1024
        # with (>= ...).  This is completely invalid (and failed to build),
1025
        # but does exist historically, so we have to deal with it.
1026
        # SourcePackageReleaseDscBinariesUpdater leaves such fields well
1027
        # alone.
1028
        LaunchpadZopelessLayer.switchDbUser('testadmin')
14625.2.5 by Colin Watson
Simplify test as suggested by Gavin Panella.
1029
        spr = self.factory.makeSourcePackageRelease(
1030
            dsc_binaries="one (>= 1), two")
14625.2.2 by Colin Watson
Add garbo job to clean up broken SPR.dsc_binaries values.
1031
        transaction.commit()
1032
        self.runDaily()
14625.2.5 by Colin Watson
Simplify test as suggested by Gavin Panella.
1033
        self.assertEqual("one (>= 1), two", spr.dsc_binaries)
14625.2.2 by Colin Watson
Add garbo job to clean up broken SPR.dsc_binaries values.
1034
8758.4.18 by Stuart Bishop
Remove LoginToken rows older than 1 year
1035
1036
class TestGarboTasks(TestCaseWithFactory):
1037
    layer = LaunchpadZopelessLayer
1038
1039
    def test_LoginTokenPruner(self):
1040
        store = IMasterStore(LoginToken)
1041
        now = datetime.now(UTC)
1042
        LaunchpadZopelessLayer.switchDbUser('testadmin')
1043
1044
        # It is configured as a daily task.
1045
        self.assertTrue(
1046
            LoginTokenPruner in DailyDatabaseGarbageCollector.tunable_loops)
1047
1048
        # Create a token that will be pruned.
1049
        old_token = LoginToken(
1050
            email='whatever', tokentype=LoginTokenType.NEWACCOUNT)
1051
        old_token.date_created = now - timedelta(days=666)
1052
        old_token_id = old_token.id
1053
        store.add(old_token)
1054
1055
        # Create a token that will not be pruned.
1056
        current_token = LoginToken(
1057
            email='whatever', tokentype=LoginTokenType.NEWACCOUNT)
1058
        current_token_id = current_token.id
1059
        store.add(current_token)
1060
1061
        # Run the pruner. Batching is tested by the BulkPruner tests so
1062
        # no need to repeat here.
1063
        LaunchpadZopelessLayer.switchDbUser('garbo_daily')
1064
        pruner = LoginTokenPruner(logging.getLogger('garbo'))
1065
        while not pruner.isDone():
1066
            pruner(10)
1067
        pruner.cleanUp()
1068
1069
        # Only the old LoginToken is gone.
1070
        self.assertEqual(
1071
            store.find(LoginToken, id=old_token_id).count(), 0)
1072
        self.assertEqual(
1073
            store.find(LoginToken, id=current_token_id).count(), 1)