Merge lp:~exarkun/divmod.org/pop3-grabber-deletes into lp:divmod.org
- pop3-grabber-deletes
- Merge into trunk
Status: | Work in progress |
---|---|
Proposed branch: | lp:~exarkun/divmod.org/pop3-grabber-deletes |
Merge into: | lp:divmod.org |
Diff against target: |
909 lines (+611/-85) 4 files modified
Quotient/xquotient/grabber.py (+166/-51) Quotient/xquotient/test/historic/stub_pop3uid1to2.py (+37/-0) Quotient/xquotient/test/historic/test_pop3uid1to2.py (+32/-0) Quotient/xquotient/test/test_grabber.py (+376/-34) |
To merge this branch: | bzr merge lp:~exarkun/divmod.org/pop3-grabber-deletes |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Divmod-dev | Pending | ||
Review via email: mp+132756@code.launchpad.net |
Commit message
Description of the change
This implements deletion of messages from POP3 servers at least one week after Quotient grabs them.
- 2713. By Jean-Paul Calderone
-
Limit the number of results from any single call to shouldDelete to around 1000 - avoids tripping over a SQLite3 limitation, and limiting the number of results is better for large mailboxes anyway.
Unmerged revisions
- 2713. By Jean-Paul Calderone
-
Limit the number of results from any single call to shouldDelete to around 1000 - avoids tripping over a SQLite3 limitation, and limiting the number of results is better for large mailboxes anyway.
- 2712. By Jean-Paul Calderone
-
Index on boolean column of questionable value; removing it causes no test failures.
- 2711. By Jean-Paul Calderone
-
Query complexity tests for shouldDelete, and a compound index to make them pass.
- 2710. By Jean-Paul Calderone
-
Avoid grabbing before POP3UID upgrade is complete
- 2709. By Jean-Paul Calderone
-
Upgrader for old POP3UID items.
- 2708. By Jean-Paul Calderone
-
Fix markDeleted to disregard unrelated grabber state
- 2707. By Jean-Paul Calderone
-
Fix shouldDelete to only pay attention to our own POP3UIDs
- 2706. By Jean-Paul Calderone
-
Handle timeouts/lost connections during DELE command
- 2705. By Jean-Paul Calderone
-
Delete the POP3UID objects once the messages are deleted from the server.
- 2704. By Jean-Paul Calderone
-
Kind of gross test for the protocol integration, and the simple implementation change to make it pass
Preview Diff
1 | === modified file 'Quotient/xquotient/grabber.py' | |||
2 | --- Quotient/xquotient/grabber.py 2012-05-11 14:05:29 +0000 | |||
3 | +++ Quotient/xquotient/grabber.py 2013-01-02 01:49:22 +0000 | |||
4 | @@ -3,7 +3,7 @@ | |||
5 | 3 | from epsilon import hotfix | 3 | from epsilon import hotfix |
6 | 4 | hotfix.require('twisted', 'deferredgenerator_tfailure') | 4 | hotfix.require('twisted', 'deferredgenerator_tfailure') |
7 | 5 | 5 | ||
9 | 6 | import time, datetime | 6 | import time, datetime, functools |
10 | 7 | 7 | ||
11 | 8 | from twisted.mail import pop3, pop3client | 8 | from twisted.mail import pop3, pop3client |
12 | 9 | from twisted.internet import protocol, defer, ssl, error | 9 | from twisted.internet import protocol, defer, ssl, error |
13 | @@ -161,6 +161,12 @@ | |||
14 | 161 | 161 | ||
15 | 162 | 162 | ||
16 | 163 | class POP3UID(item.Item): | 163 | class POP3UID(item.Item): |
17 | 164 | schemaVersion = 2 | ||
18 | 165 | |||
19 | 166 | retrieved = attributes.timestamp(doc=""" | ||
20 | 167 | When this POP3 UID was retrieved (or when retrieval failed). | ||
21 | 168 | """, allowNone=False) | ||
22 | 169 | |||
23 | 164 | grabberID = attributes.text(doc=""" | 170 | grabberID = attributes.text(doc=""" |
24 | 165 | A string identifying the email-address/port parts of a | 171 | A string identifying the email-address/port parts of a |
25 | 166 | configured grabber | 172 | configured grabber |
26 | @@ -173,14 +179,33 @@ | |||
27 | 173 | failed = attributes.boolean(doc=""" | 179 | failed = attributes.boolean(doc=""" |
28 | 174 | When set, indicates that an attempt was made to retrieve this UID, | 180 | When set, indicates that an attempt was made to retrieve this UID, |
29 | 175 | but for some reason was unsuccessful. | 181 | but for some reason was unsuccessful. |
32 | 176 | """, indexed=True, default=False) | 182 | """, default=False) |
33 | 177 | 183 | ||
34 | 184 | attributes.compoundIndex(grabberID, retrieved) | ||
35 | 185 | |||
36 | 186 | |||
37 | 187 | def _pop3uid1to2(old): | ||
38 | 188 | return old.upgradeVersion( | ||
39 | 189 | POP3UID.typeName, 1, 2, | ||
40 | 190 | value=old.value, failed=old.failed, grabberID=old.grabberID, | ||
41 | 191 | retrieved=extime.Time()) | ||
42 | 192 | registerUpgrader(_pop3uid1to2, POP3UID.typeName, 1, 2) | ||
43 | 193 | |||
44 | 194 | POP3UIDv1 = item.declareLegacyItem(POP3UID.typeName, 1, dict( | ||
45 | 195 | grabberID=attributes.text(indexed=True), | ||
46 | 196 | value=attributes.bytes(indexed=True), | ||
47 | 197 | failed=attributes.boolean(indexed=True, default=False))) | ||
48 | 178 | 198 | ||
49 | 179 | 199 | ||
50 | 180 | class POP3Grabber(item.Item): | 200 | class POP3Grabber(item.Item): |
51 | 181 | """ | 201 | """ |
52 | 182 | Item for retrieving email messages from a remote POP server. | 202 | Item for retrieving email messages from a remote POP server. |
53 | 183 | """ | 203 | """ |
54 | 204 | DELETE_DELAY = datetime.timedelta(days=7) | ||
55 | 205 | |||
56 | 206 | now = attributes.inmemory(doc=""" | ||
57 | 207 | A callable returning a Time instance representing the current time. | ||
58 | 208 | """) | ||
59 | 184 | 209 | ||
60 | 185 | config = attributes.reference(doc=""" | 210 | config = attributes.reference(doc=""" |
61 | 186 | The L{GrabberConfiguration} which created this grabber. | 211 | The L{GrabberConfiguration} which created this grabber. |
62 | @@ -271,6 +296,7 @@ | |||
63 | 271 | self._pop3uids = None | 296 | self._pop3uids = None |
64 | 272 | self.running = False | 297 | self.running = False |
65 | 273 | self.protocol = None | 298 | self.protocol = None |
66 | 299 | self.now = extime.Time | ||
67 | 274 | if self.status is None: | 300 | if self.status is None: |
68 | 275 | self.status = Status(store=self.store, message=u'idle') | 301 | self.status = Status(store=self.store, message=u'idle') |
69 | 276 | 302 | ||
70 | @@ -288,6 +314,14 @@ | |||
71 | 288 | # Don't run concurrently, ever. | 314 | # Don't run concurrently, ever. |
72 | 289 | if self.running: | 315 | if self.running: |
73 | 290 | return | 316 | return |
74 | 317 | |||
75 | 318 | # Don't run while POP3UIDs are being upgraded. Any that have not yet | ||
76 | 319 | # been upgraded won't be returned from query(POP3UID) calls, which will | ||
77 | 320 | # confuse the logic about which messages to download. Eventually | ||
78 | 321 | # they'll all be upgraded and we'll resume grabbing. | ||
79 | 322 | if self.store.query(POP3UIDv1).count(): | ||
80 | 323 | return | ||
81 | 324 | |||
82 | 291 | self.running = True | 325 | self.running = True |
83 | 292 | 326 | ||
84 | 293 | from twisted.internet import reactor | 327 | from twisted.internet import reactor |
85 | @@ -340,10 +374,35 @@ | |||
86 | 340 | grabberID = property(_grabberID) | 374 | grabberID = property(_grabberID) |
87 | 341 | 375 | ||
88 | 342 | 376 | ||
93 | 343 | def shouldRetrieve(self, uidList): | 377 | def shouldDelete(self, uidList): |
94 | 344 | """ | 378 | """ |
95 | 345 | Return a list of (index, uid) pairs from C{uidList} which have not | 379 | Return a list of (index, uid) pairs from C{uidList} which were |
96 | 346 | already been grabbed. | 380 | downloaded long enough ago that they can be deleted now. |
97 | 381 | """ | ||
98 | 382 | # Find at most 996 of them. Combined with the other query variables in | ||
99 | 383 | # the statement below, this reaches the SQLite3 query variable limit. | ||
100 | 384 | # Any additional will be picked up in the future. | ||
101 | 385 | uidList = uidList[:996] | ||
102 | 386 | |||
103 | 387 | # And further limit them to POP3UIDs which were retrieved at least | ||
104 | 388 | # DELETE_DELAY ago. Failed attempts do not count. | ||
105 | 389 | where = attributes.AND( | ||
106 | 390 | POP3UID.grabberID == self.grabberID, | ||
107 | 391 | POP3UID.retrieved < self.now() - self.DELETE_DELAY, | ||
108 | 392 | POP3UID.failed == False, | ||
109 | 393 | POP3UID.value.oneOf([pair[1] for pair in uidList])) | ||
110 | 394 | |||
111 | 395 | # Here are the server-side POP3 UIDs which we have downloaded and which | ||
112 | 396 | # are old enough, so we should delete them. | ||
113 | 397 | pop3uids = set(self.store.query(POP3UID, where).getColumn("value")) | ||
114 | 398 | |||
115 | 399 | return [pair for pair in uidList if pair[1] in pop3uids] | ||
116 | 400 | |||
117 | 401 | |||
118 | 402 | def _getPOP3UIDs(self): | ||
119 | 403 | """ | ||
120 | 404 | Return all the L{POP3UID} instances created by this grabber which still | ||
121 | 405 | exist, perhaps from an in-memory cache. | ||
122 | 347 | """ | 406 | """ |
123 | 348 | if self._pop3uids is None: | 407 | if self._pop3uids is None: |
124 | 349 | before = time.time() | 408 | before = time.time() |
125 | @@ -352,8 +411,17 @@ | |||
126 | 352 | self._pop3uids = set(self.store.query(POP3UID, POP3UID.grabberID == self.grabberID).getColumn("value")) | 411 | self._pop3uids = set(self.store.query(POP3UID, POP3UID.grabberID == self.grabberID).getColumn("value")) |
127 | 353 | after = time.time() | 412 | after = time.time() |
128 | 354 | log.msg(interface=iaxiom.IStatEvent, stat_pop3uid_load_time=after - before) | 413 | log.msg(interface=iaxiom.IStatEvent, stat_pop3uid_load_time=after - before) |
129 | 414 | return self._pop3uids | ||
130 | 415 | |||
131 | 416 | |||
132 | 417 | def shouldRetrieve(self, uidList): | ||
133 | 418 | """ | ||
134 | 419 | Return a list of (index, uid) pairs from C{uidList} which have not | ||
135 | 420 | already been grabbed. | ||
136 | 421 | """ | ||
137 | 422 | pop3uids = self._getPOP3UIDs() | ||
138 | 355 | log.msg(interface=iaxiom.IStatEvent, stat_pop3uid_check=len(uidList)) | 423 | log.msg(interface=iaxiom.IStatEvent, stat_pop3uid_check=len(uidList)) |
140 | 356 | return [pair for pair in uidList if pair[1] not in self._pop3uids] | 424 | return [pair for pair in uidList if pair[1] not in pop3uids] |
141 | 357 | 425 | ||
142 | 358 | 426 | ||
143 | 359 | def markSuccess(self, uid, msg): | 427 | def markSuccess(self, uid, msg): |
144 | @@ -378,17 +446,33 @@ | |||
145 | 378 | msg.archive() | 446 | msg.archive() |
146 | 379 | log.msg(interface=iaxiom.IStatEvent, stat_messages_grabbed=1, | 447 | log.msg(interface=iaxiom.IStatEvent, stat_messages_grabbed=1, |
147 | 380 | userstore=self.store) | 448 | userstore=self.store) |
149 | 381 | POP3UID(store=self.store, grabberID=self.grabberID, value=uid) | 449 | POP3UID( |
150 | 450 | store=self.store, | ||
151 | 451 | grabberID=self.grabberID, | ||
152 | 452 | value=uid, | ||
153 | 453 | retrieved=self.now()) | ||
154 | 382 | if self._pop3uids is not None: | 454 | if self._pop3uids is not None: |
155 | 383 | self._pop3uids.add(uid) | 455 | self._pop3uids.add(uid) |
156 | 384 | 456 | ||
157 | 385 | 457 | ||
158 | 386 | def markFailure(self, uid, err): | 458 | def markFailure(self, uid, err): |
160 | 387 | POP3UID(store=self.store, grabberID=self.grabberID, value=uid, failed=True) | 459 | POP3UID( |
161 | 460 | store=self.store, | ||
162 | 461 | grabberID=self.grabberID, | ||
163 | 462 | value=uid, | ||
164 | 463 | retrieved=self.now(), | ||
165 | 464 | failed=True) | ||
166 | 388 | if self._pop3uids is not None: | 465 | if self._pop3uids is not None: |
167 | 389 | self._pop3uids.add(uid) | 466 | self._pop3uids.add(uid) |
168 | 390 | 467 | ||
169 | 391 | 468 | ||
170 | 469 | def markDeleted(self, uid): | ||
171 | 470 | where = attributes.AND( | ||
172 | 471 | POP3UID.value == uid, POP3UID.grabberID == self.grabberID) | ||
173 | 472 | query = self.store.query(POP3UID, where) | ||
174 | 473 | query.deleteFromStore() | ||
175 | 474 | |||
176 | 475 | |||
177 | 392 | 476 | ||
178 | 393 | class POP3GrabberProtocol(pop3.AdvancedPOP3Client): | 477 | class POP3GrabberProtocol(pop3.AdvancedPOP3Client): |
179 | 394 | _rate = 50 | 478 | _rate = 50 |
180 | @@ -479,6 +563,9 @@ | |||
181 | 479 | # All the (index, uid) pairs which should be retrieved | 563 | # All the (index, uid) pairs which should be retrieved |
182 | 480 | uidList = [] | 564 | uidList = [] |
183 | 481 | 565 | ||
184 | 566 | # All of the (index, uid) pairs which should be deleted | ||
185 | 567 | uidDeleteList = [] | ||
186 | 568 | |||
187 | 482 | # Consumer for listUID - adds to the working set and processes | 569 | # Consumer for listUID - adds to the working set and processes |
188 | 483 | # a batch if appropriate. | 570 | # a batch if appropriate. |
189 | 484 | def consumeUIDLine(ent): | 571 | def consumeUIDLine(ent): |
190 | @@ -487,9 +574,14 @@ | |||
191 | 487 | processBatch() | 574 | processBatch() |
192 | 488 | 575 | ||
193 | 489 | def processBatch(): | 576 | def processBatch(): |
197 | 490 | L = self.shouldRetrieve(uidWorkingSet) | 577 | toRetrieve = self.shouldRetrieve(uidWorkingSet) |
198 | 491 | L.sort() | 578 | toRetrieve.sort() |
199 | 492 | uidList.extend(L) | 579 | uidList.extend(toRetrieve) |
200 | 580 | |||
201 | 581 | toDelete = self.shouldDelete(uidWorkingSet) | ||
202 | 582 | toDelete.sort() | ||
203 | 583 | uidDeleteList.extend(toDelete) | ||
204 | 584 | |||
205 | 493 | del uidWorkingSet[:] | 585 | del uidWorkingSet[:] |
206 | 494 | 586 | ||
207 | 495 | 587 | ||
208 | @@ -555,6 +647,17 @@ | |||
209 | 555 | else: | 647 | else: |
210 | 556 | self.markSuccess(uid, rece.message) | 648 | self.markSuccess(uid, rece.message) |
211 | 557 | 649 | ||
212 | 650 | # Delete any old messages that should now be deleted | ||
213 | 651 | for (index, uid) in uidDeleteList: | ||
214 | 652 | d = defer.waitForDeferred(self.delete(index)) | ||
215 | 653 | yield d | ||
216 | 654 | try: | ||
217 | 655 | d.getResult() | ||
218 | 656 | except (error.ConnectionDone, error.ConnectionLost): | ||
219 | 657 | return | ||
220 | 658 | |||
221 | 659 | self.markDeleted(uid) | ||
222 | 660 | |||
223 | 558 | self.setStatus(u"Logging out...") | 661 | self.setStatus(u"Logging out...") |
224 | 559 | d = defer.waitForDeferred(self.quit()) | 662 | d = defer.waitForDeferred(self.quit()) |
225 | 560 | yield d | 663 | yield d |
226 | @@ -584,56 +687,68 @@ | |||
227 | 584 | 687 | ||
228 | 585 | 688 | ||
229 | 586 | 689 | ||
230 | 690 | def _requiresGrabberItem(f): | ||
231 | 691 | """ | ||
232 | 692 | Decorator for a method on ControlledPOP3GrabberProtocol which makes it safe | ||
233 | 693 | to call even after the connection has been lost. | ||
234 | 694 | """ | ||
235 | 695 | @functools.wraps(f) | ||
236 | 696 | def safe(self, *args, **kwargs): | ||
237 | 697 | if self.grabber is not None: | ||
238 | 698 | return self.grabber.store.transact(f, self, *args, **kwargs) | ||
239 | 699 | return safe | ||
240 | 700 | |||
241 | 701 | |||
242 | 702 | |||
243 | 587 | class ControlledPOP3GrabberProtocol(POP3GrabberProtocol): | 703 | class ControlledPOP3GrabberProtocol(POP3GrabberProtocol): |
248 | 588 | def _transact(self, *a, **kw): | 704 | _transient = False |
249 | 589 | return self.grabber.store.transact(*a, **kw) | 705 | def transientFailure(self, f): |
250 | 590 | 706 | self._transient = True | |
251 | 591 | 707 | ||
252 | 708 | |||
253 | 709 | @_requiresGrabberItem | ||
254 | 592 | def getSource(self): | 710 | def getSource(self): |
255 | 593 | return u'pop3://' + self.grabber.grabberID | 711 | return u'pop3://' + self.grabber.grabberID |
256 | 594 | 712 | ||
257 | 595 | 713 | ||
258 | 714 | @_requiresGrabberItem | ||
259 | 596 | def setStatus(self, msg, success=True): | 715 | def setStatus(self, msg, success=True): |
264 | 597 | if self.grabber is not None: | 716 | return self.grabber.status.setStatus(msg, success) |
265 | 598 | self._transact(self.grabber.status.setStatus, msg, success) | 717 | |
266 | 599 | 718 | ||
267 | 600 | 719 | @_requiresGrabberItem | |
268 | 601 | def shouldRetrieve(self, uidList): | 720 | def shouldRetrieve(self, uidList): |
273 | 602 | if self.grabber is not None: | 721 | return self.grabber.shouldRetrieve(uidList) |
274 | 603 | return self._transact(self.grabber.shouldRetrieve, uidList) | 722 | |
275 | 604 | 723 | ||
276 | 605 | 724 | @_requiresGrabberItem | |
277 | 725 | def shouldDelete(self, uidList): | ||
278 | 726 | return self.grabber.shouldDelete(uidList) | ||
279 | 727 | |||
280 | 728 | |||
281 | 729 | @_requiresGrabberItem | ||
282 | 606 | def createMIMEReceiver(self, source): | 730 | def createMIMEReceiver(self, source): |
290 | 607 | if self.grabber is not None: | 731 | agent = self.grabber.config.deliveryAgent |
291 | 608 | def createIt(): | 732 | return agent.createMIMEReceiver(source) |
292 | 609 | agent = self.grabber.config.deliveryAgent | 733 | |
293 | 610 | return agent.createMIMEReceiver(source) | 734 | |
294 | 611 | return self._transact(createIt) | 735 | @_requiresGrabberItem |
288 | 612 | |||
289 | 613 | |||
295 | 614 | def markSuccess(self, uid, msg): | 736 | def markSuccess(self, uid, msg): |
300 | 615 | if self.grabber is not None: | 737 | return self.grabber.markSuccess(uid, msg) |
301 | 616 | return self._transact(self.grabber.markSuccess, uid, msg) | 738 | |
302 | 617 | 739 | ||
303 | 618 | 740 | @_requiresGrabberItem | |
304 | 619 | def markFailure(self, uid, reason): | 741 | def markFailure(self, uid, reason): |
309 | 620 | if self.grabber is not None: | 742 | return self.grabber.markFailure(uid, reason) |
310 | 621 | return self._transact(self.grabber.markFailure, uid, reason) | 743 | |
311 | 622 | 744 | ||
312 | 623 | 745 | @_requiresGrabberItem | |
313 | 624 | def paused(self): | 746 | def paused(self): |
323 | 625 | if self.grabber is not None: | 747 | return self.grabber.paused |
324 | 626 | return self.grabber.paused | 748 | |
325 | 627 | 749 | ||
326 | 628 | 750 | @_requiresGrabberItem | |
318 | 629 | _transient = False | ||
319 | 630 | def transientFailure(self, f): | ||
320 | 631 | self._transient = True | ||
321 | 632 | |||
322 | 633 | |||
327 | 634 | def stoppedRunning(self): | 751 | def stoppedRunning(self): |
328 | 635 | if self.grabber is None: | ||
329 | 636 | return | ||
330 | 637 | self.grabber.running = False | 752 | self.grabber.running = False |
331 | 638 | if self._transient: | 753 | if self._transient: |
332 | 639 | iaxiom.IScheduler(self.grabber.store).reschedule( | 754 | iaxiom.IScheduler(self.grabber.store).reschedule( |
333 | 640 | 755 | ||
334 | === added file 'Quotient/xquotient/test/historic/pop3uid1to2.axiom.tbz2' | |||
335 | 641 | Binary files Quotient/xquotient/test/historic/pop3uid1to2.axiom.tbz2 1970-01-01 00:00:00 +0000 and Quotient/xquotient/test/historic/pop3uid1to2.axiom.tbz2 2013-01-02 01:49:22 +0000 differ | 756 | Binary files Quotient/xquotient/test/historic/pop3uid1to2.axiom.tbz2 1970-01-01 00:00:00 +0000 and Quotient/xquotient/test/historic/pop3uid1to2.axiom.tbz2 2013-01-02 01:49:22 +0000 differ |
336 | === added file 'Quotient/xquotient/test/historic/stub_pop3uid1to2.py' | |||
337 | --- Quotient/xquotient/test/historic/stub_pop3uid1to2.py 1970-01-01 00:00:00 +0000 | |||
338 | +++ Quotient/xquotient/test/historic/stub_pop3uid1to2.py 2013-01-02 01:49:22 +0000 | |||
339 | @@ -0,0 +1,37 @@ | |||
340 | 1 | # -*- test-case-name: xquotient.test.historic.test_pop3uid1to2 -*- | ||
341 | 2 | |||
342 | 3 | """ | ||
343 | 4 | Create stub database for upgrade of L{xquotient.grabber.POP3UID} from version 1 | ||
344 | 5 | to version 2. | ||
345 | 6 | """ | ||
346 | 7 | |||
347 | 8 | from axiom.test.historic.stubloader import saveStub | ||
348 | 9 | |||
349 | 10 | from axiom.userbase import LoginSystem | ||
350 | 11 | from axiom.dependency import installOn | ||
351 | 12 | |||
352 | 13 | from xquotient.grabber import POP3UID | ||
353 | 14 | |||
354 | 15 | VALUE = b"12345678abcdefgh" | ||
355 | 16 | FAILED = False | ||
356 | 17 | GRABBER_ID = u"alice@example.com:1234" | ||
357 | 18 | |||
358 | 19 | def createDatabase(s): | ||
359 | 20 | """ | ||
360 | 21 | Create an account in the given store and create a POP3UID item in it. | ||
361 | 22 | """ | ||
362 | 23 | loginSystem = LoginSystem(store=s) | ||
363 | 24 | installOn(loginSystem, s) | ||
364 | 25 | |||
365 | 26 | account = loginSystem.addAccount(u'testuser', u'localhost', None) | ||
366 | 27 | subStore = account.avatars.open() | ||
367 | 28 | |||
368 | 29 | POP3UID( | ||
369 | 30 | store=subStore, | ||
370 | 31 | value=VALUE, | ||
371 | 32 | failed=FAILED, | ||
372 | 33 | grabberID=GRABBER_ID) | ||
373 | 34 | |||
374 | 35 | |||
375 | 36 | if __name__ == '__main__': | ||
376 | 37 | saveStub(createDatabase, 'exarkun@twistedmatrix.com-20120913121256-tg7d6l1w3rkpfehr') | ||
377 | 0 | 38 | ||
378 | === added file 'Quotient/xquotient/test/historic/test_pop3uid1to2.py' | |||
379 | --- Quotient/xquotient/test/historic/test_pop3uid1to2.py 1970-01-01 00:00:00 +0000 | |||
380 | +++ Quotient/xquotient/test/historic/test_pop3uid1to2.py 2013-01-02 01:49:22 +0000 | |||
381 | @@ -0,0 +1,32 @@ | |||
382 | 1 | |||
383 | 2 | """ | ||
384 | 3 | Test that a version 1 POP3UID is unchanged by the upgrade except that it gains a | ||
385 | 4 | value for the new C{retrieved} attribute set to something near the current time. | ||
386 | 5 | """ | ||
387 | 6 | |||
388 | 7 | from epsilon.extime import Time | ||
389 | 8 | |||
390 | 9 | from axiom.userbase import LoginSystem | ||
391 | 10 | from axiom.test.historic.stubloader import StubbedTest | ||
392 | 11 | |||
393 | 12 | from xquotient.test.historic.stub_pop3uid1to2 import VALUE, FAILED, GRABBER_ID | ||
394 | 13 | from xquotient.grabber import POP3UID | ||
395 | 14 | |||
396 | 15 | class POP3UIDUpgradeTestCase(StubbedTest): | ||
397 | 16 | def test_attributes(self): | ||
398 | 17 | loginSystem = self.store.findUnique(LoginSystem) | ||
399 | 18 | account = loginSystem.accountByAddress(u'testuser', u'localhost') | ||
400 | 19 | subStore = account.avatars.open() | ||
401 | 20 | |||
402 | 21 | d = subStore.whenFullyUpgraded() | ||
403 | 22 | def upgraded(ignored): | ||
404 | 23 | [pop3uid] = list(subStore.query(POP3UID)) | ||
405 | 24 | self.assertEqual(VALUE, pop3uid.value) | ||
406 | 25 | self.assertEqual(FAILED, pop3uid.failed) | ||
407 | 26 | self.assertEqual(GRABBER_ID, pop3uid.grabberID) | ||
408 | 27 | |||
409 | 28 | # This will be close enough. | ||
410 | 29 | elapsed = (Time() - pop3uid.retrieved).total_seconds() | ||
411 | 30 | self.assertTrue(abs(elapsed) < 60) | ||
412 | 31 | d.addCallback(upgraded) | ||
413 | 32 | return d | ||
414 | 0 | 33 | ||
415 | === modified file 'Quotient/xquotient/test/test_grabber.py' | |||
416 | --- Quotient/xquotient/test/test_grabber.py 2012-05-11 14:05:29 +0000 | |||
417 | +++ Quotient/xquotient/test/test_grabber.py 2013-01-02 01:49:22 +0000 | |||
418 | @@ -3,8 +3,11 @@ | |||
419 | 3 | 3 | ||
420 | 4 | from datetime import timedelta | 4 | from datetime import timedelta |
421 | 5 | 5 | ||
422 | 6 | from zope.interface import directlyProvides | ||
423 | 7 | |||
424 | 6 | from twisted.trial import unittest | 8 | from twisted.trial import unittest |
425 | 7 | from twisted.internet import defer, error | 9 | from twisted.internet import defer, error |
426 | 10 | from twisted.internet.interfaces import ISSLTransport | ||
427 | 8 | from twisted.mail import pop3 | 11 | from twisted.mail import pop3 |
428 | 9 | from twisted.cred import error as ecred | 12 | from twisted.cred import error as ecred |
429 | 10 | from twisted.test.proto_helpers import StringTransport | 13 | from twisted.test.proto_helpers import StringTransport |
430 | @@ -15,6 +18,7 @@ | |||
431 | 15 | from epsilon.test import iosim | 18 | from epsilon.test import iosim |
432 | 16 | 19 | ||
433 | 17 | from axiom import iaxiom, store, substore, scheduler | 20 | from axiom import iaxiom, store, substore, scheduler |
434 | 21 | from axiom.test.util import QueryCounter | ||
435 | 18 | 22 | ||
436 | 19 | from xquotient import grabber, mimepart | 23 | from xquotient import grabber, mimepart |
437 | 20 | 24 | ||
438 | @@ -50,6 +54,8 @@ | |||
439 | 50 | def connectionMade(self): | 54 | def connectionMade(self): |
440 | 51 | grabber.POP3GrabberProtocol.connectionMade(self) | 55 | grabber.POP3GrabberProtocol.connectionMade(self) |
441 | 52 | self.events = [] | 56 | self.events = [] |
442 | 57 | self.uidsForDeletion = set() | ||
443 | 58 | self.uidsNotForRetrieval = set() | ||
444 | 53 | 59 | ||
445 | 54 | 60 | ||
446 | 55 | def getSource(self): | 61 | def getSource(self): |
447 | @@ -62,8 +68,13 @@ | |||
448 | 62 | 68 | ||
449 | 63 | 69 | ||
450 | 64 | def shouldRetrieve(self, uidList): | 70 | def shouldRetrieve(self, uidList): |
453 | 65 | self.events.append(('retrieve', uidList)) | 71 | self.events.append(('retrieve', list(uidList))) |
454 | 66 | return list(uidList) | 72 | return [pair for pair in uidList if pair[1] not in self.uidsNotForRetrieval] |
455 | 73 | |||
456 | 74 | |||
457 | 75 | def shouldDelete(self, uidList): | ||
458 | 76 | self.events.append(('delete', list(uidList))) | ||
459 | 77 | return [pair for pair in uidList if pair[1] in self.uidsForDeletion] | ||
460 | 67 | 78 | ||
461 | 68 | 79 | ||
462 | 69 | def createMIMEReceiver(self, source): | 80 | def createMIMEReceiver(self, source): |
463 | @@ -80,6 +91,10 @@ | |||
464 | 80 | self.events.append(('failure', uid, reason)) | 91 | self.events.append(('failure', uid, reason)) |
465 | 81 | 92 | ||
466 | 82 | 93 | ||
467 | 94 | def markDeleted(self, uid): | ||
468 | 95 | self.events.append(('markDeleted', uid)) | ||
469 | 96 | |||
470 | 97 | |||
471 | 83 | def paused(self): | 98 | def paused(self): |
472 | 84 | self.events.append(('paused',)) | 99 | self.events.append(('paused',)) |
473 | 85 | return False | 100 | return False |
474 | @@ -229,6 +244,52 @@ | |||
475 | 229 | 'stopped') | 244 | 'stopped') |
476 | 230 | 245 | ||
477 | 231 | 246 | ||
478 | 247 | def test_deletion(self): | ||
479 | 248 | """ | ||
480 | 249 | Messages indicated by C{shouldDelete} to be ready for deleted are | ||
481 | 250 | deleted using the I{DELE} POP3 protocol action. | ||
482 | 251 | """ | ||
483 | 252 | transport = StringTransport() | ||
484 | 253 | # Convince the client to log in | ||
485 | 254 | directlyProvides(transport, ISSLTransport) | ||
486 | 255 | |||
487 | 256 | self.client.makeConnection(transport) | ||
488 | 257 | self.addCleanup(self.client.connectionLost, error.ConnectionLost("Simulated")) | ||
489 | 258 | |||
490 | 259 | self.client.uidsForDeletion.add(b'xyz') | ||
491 | 260 | self.client.uidsNotForRetrieval.add(b'abc') | ||
492 | 261 | self.client.uidsNotForRetrieval.add(b'xyz') | ||
493 | 262 | |||
494 | 263 | # Server greeting | ||
495 | 264 | self.client.dataReceived("+OK Hello\r\n") | ||
496 | 265 | # CAPA response | ||
497 | 266 | self.client.dataReceived("+OK\r\nUSER\r\nUIDL\r\n.\r\n") | ||
498 | 267 | # USER response | ||
499 | 268 | self.client.dataReceived("+OK\r\n") | ||
500 | 269 | # PASS response | ||
501 | 270 | self.client.dataReceived("+OK\r\n") | ||
502 | 271 | |||
503 | 272 | del self.client.events[:] | ||
504 | 273 | transport.clear() | ||
505 | 274 | |||
506 | 275 | # UIDL response | ||
507 | 276 | self.client.dataReceived('+OK \r\n1 abc\r\n3 xyz\r\n.\r\n') | ||
508 | 277 | |||
509 | 278 | # Protocol should consult shouldDelete with the UIDs and start issuing | ||
510 | 279 | # delete commands. | ||
511 | 280 | self.assertEquals( | ||
512 | 281 | [('delete', [(0, 'abc'), (2, 'xyz')])], | ||
513 | 282 | [event for event in self.client.events if event[0] == 'delete']) | ||
514 | 283 | self.assertEqual("DELE 3\r\n", transport.value()) | ||
515 | 284 | |||
516 | 285 | del self.client.events[:] | ||
517 | 286 | |||
518 | 287 | # DELE response | ||
519 | 288 | self.client.dataReceived("+OK\r\n") | ||
520 | 289 | |||
521 | 290 | self.assertEquals(('markDeleted', 'xyz'), self.client.events[0]) | ||
522 | 291 | |||
523 | 292 | |||
524 | 232 | def testLineTooLong(self): | 293 | def testLineTooLong(self): |
525 | 233 | """ | 294 | """ |
526 | 234 | Make sure a message illegally served with a line longer than we will | 295 | Make sure a message illegally served with a line longer than we will |
527 | @@ -400,21 +461,38 @@ | |||
528 | 400 | self.assertTrue(scheduled[0] <= extime.Time()) | 461 | self.assertTrue(scheduled[0] <= extime.Time()) |
529 | 401 | 462 | ||
530 | 402 | 463 | ||
531 | 464 | def _timeoutTest(self, exchange): | ||
532 | 465 | """ | ||
533 | 466 | Exercise handling of a connection timeout at some phase of the | ||
534 | 467 | interaction. | ||
535 | 468 | """ | ||
536 | 469 | transport = StringTransport() | ||
537 | 470 | factory = grabber.POP3GrabberFactory(self.grabberItem, False) | ||
538 | 471 | protocol = factory.buildProtocol(None) | ||
539 | 472 | protocol.allowInsecureLogin = True | ||
540 | 473 | protocol.makeConnection(transport) | ||
541 | 474 | |||
542 | 475 | for (serverMessage, clientMessage) in exchange: | ||
543 | 476 | protocol.dataReceived(serverMessage) | ||
544 | 477 | self.assertEqual(clientMessage, transport.value()) | ||
545 | 478 | transport.clear() | ||
546 | 479 | |||
547 | 480 | protocol.timeoutConnection() | ||
548 | 481 | self.assertTrue(transport.disconnecting) | ||
549 | 482 | protocol.connectionLost(Failure(error.ConnectionLost("Simulated"))) | ||
550 | 483 | |||
551 | 484 | self.assertEqual( | ||
552 | 485 | self.grabberItem.status.message, | ||
553 | 486 | u"Timed out waiting for server response.") | ||
554 | 487 | |||
555 | 488 | |||
556 | 403 | def test_stoppedRunningAfterTimeout(self): | 489 | def test_stoppedRunningAfterTimeout(self): |
557 | 404 | """ | 490 | """ |
558 | 405 | When L{ControlledPOP3GrabberProtocol} times out the connection | 491 | When L{ControlledPOP3GrabberProtocol} times out the connection |
559 | 406 | due to inactivity, the controlling grabber's status is set to | 492 | due to inactivity, the controlling grabber's status is set to |
560 | 407 | reflect this. | 493 | reflect this. |
561 | 408 | """ | 494 | """ |
571 | 409 | factory = grabber.POP3GrabberFactory(self.grabberItem, False) | 495 | self._timeoutTest([]) |
563 | 410 | protocol = factory.buildProtocol(None) | ||
564 | 411 | protocol.makeConnection(StringTransport()) | ||
565 | 412 | protocol.timeoutConnection() | ||
566 | 413 | protocol.connectionLost(Failure(error.ConnectionLost("Simulated"))) | ||
567 | 414 | |||
568 | 415 | self.assertEqual( | ||
569 | 416 | self.grabberItem.status.message, | ||
570 | 417 | u"Timed out waiting for server response.") | ||
572 | 418 | 496 | ||
573 | 419 | 497 | ||
574 | 420 | def test_stoppedRunningAfterListTimeout(self): | 498 | def test_stoppedRunningAfterListTimeout(self): |
575 | @@ -424,28 +502,53 @@ | |||
576 | 424 | (list UIDs) command, the controlling grabber's status is set | 502 | (list UIDs) command, the controlling grabber's status is set |
577 | 425 | to reflect this. | 503 | to reflect this. |
578 | 426 | """ | 504 | """ |
601 | 427 | factory = grabber.POP3GrabberFactory(self.grabberItem, False) | 505 | self._timeoutTest([ |
602 | 428 | protocol = factory.buildProtocol(None) | 506 | # Server greeting |
603 | 429 | protocol.allowInsecureLogin = True | 507 | (b"+OK Hello\r\n", b"CAPA\r\n"), |
604 | 430 | protocol.makeConnection(StringTransport()) | 508 | # CAPA response |
605 | 431 | # Server greeting | 509 | (b"+OK\r\nUSER\r\nUIDL\r\n.\r\n", b"USER alice\r\n"), |
606 | 432 | protocol.dataReceived("+OK Hello\r\n") | 510 | # USER response |
607 | 433 | # CAPA response | 511 | (b"+OK\r\n", b"PASS secret\r\n"), |
608 | 434 | protocol.dataReceived("+OK\r\nUSER\r\nUIDL\r\n.\r\n") | 512 | # PASS response |
609 | 435 | # USER response | 513 | (b"+OK\r\n", b"UIDL\r\n")]) |
610 | 436 | protocol.dataReceived("+OK\r\n") | 514 | |
611 | 437 | # PASS response | 515 | |
612 | 438 | protocol.dataReceived("+OK\r\n") | 516 | def test_stoppedRunningAfterDeleteTimeout(self): |
613 | 439 | # Sanity check, we should have gotten to sending the UIDL | 517 | # Set up some good state to want to delete |
614 | 440 | self.assertTrue( | 518 | uid = b'abc' |
615 | 441 | protocol.transport.value().endswith("\r\nUIDL\r\n"), | 519 | delay = self.grabberItem.DELETE_DELAY |
616 | 442 | "Failed to get to UIDL: %r" % (protocol.transport.value(),)) | 520 | future = extime.Time() |
617 | 443 | 521 | now = future - delay - timedelta(seconds=1) | |
618 | 444 | protocol.timeoutConnection() | 522 | self.grabberItem.now = lambda: now |
619 | 445 | protocol.connectionLost(Failure(error.ConnectionLost("Simulated"))) | 523 | self.grabberItem.markSuccess(uid, StubMessage()) |
620 | 446 | self.assertEqual( | 524 | now = future |
621 | 447 | self.grabberItem.status.message, | 525 | |
622 | 448 | u"Timed out waiting for server response.") | 526 | self._timeoutTest([ |
623 | 527 | # Server greeting | ||
624 | 528 | (b"+OK Hello\r\n", b"CAPA\r\n"), | ||
625 | 529 | # CAPA response | ||
626 | 530 | (b"+OK\r\nUSER\r\nUIDL\r\n.\r\n", b"USER alice\r\n"), | ||
627 | 531 | # USER response | ||
628 | 532 | (b"+OK\r\n", b"PASS secret\r\n"), | ||
629 | 533 | # PASS response | ||
630 | 534 | (b"+OK\r\n", b"UIDL\r\n"), | ||
631 | 535 | # UIDL response | ||
632 | 536 | (b"+OK\r\n1 abc\r\n.\r\n", b"DELE 1\r\n")]) | ||
633 | 537 | |||
634 | 538 | |||
635 | 539 | def test_notGrabWhileUpgrading(self): | ||
636 | 540 | """ | ||
637 | 541 | As long as any old (schemaVersion less than most recent) L{POP3UID} | ||
638 | 542 | items remain in the database, L{POP3Grabber.grab} does not try to grab | ||
639 | 543 | any messages. | ||
640 | 544 | """ | ||
641 | 545 | grabber.POP3UIDv1( | ||
642 | 546 | store=self.userStore, | ||
643 | 547 | grabberID=self.grabberItem.grabberID, | ||
644 | 548 | failed=False, | ||
645 | 549 | value=b'xyz') | ||
646 | 550 | self.grabberItem.grab() | ||
647 | 551 | self.assertFalse(self.grabberItem.running) | ||
648 | 449 | 552 | ||
649 | 450 | 553 | ||
650 | 451 | 554 | ||
651 | @@ -490,7 +593,8 @@ | |||
652 | 490 | for i in xrange(100, 200): | 593 | for i in xrange(100, 200): |
653 | 491 | grabber.POP3UID(store=self.store, | 594 | grabber.POP3UID(store=self.store, |
654 | 492 | grabberID=self.grabber.grabberID, | 595 | grabberID=self.grabber.grabberID, |
656 | 493 | value=str(i)) | 596 | value=str(i), |
657 | 597 | retrieved=extime.Time()) | ||
658 | 494 | 598 | ||
659 | 495 | 599 | ||
660 | 496 | def testShouldRetrieve(self): | 600 | def testShouldRetrieve(self): |
661 | @@ -541,6 +645,34 @@ | |||
662 | 541 | [(49, '49'), (51, '51')]) | 645 | [(49, '49'), (51, '51')]) |
663 | 542 | 646 | ||
664 | 543 | 647 | ||
665 | 648 | def test_successTimestamp(self): | ||
666 | 649 | """ | ||
667 | 650 | The L{POP3UID} instance created by L{POP3Grabber.markSuccess} has its | ||
668 | 651 | C{retrieved} attribute set to the current time as reported by | ||
669 | 652 | L{POP3Grabber.now}. | ||
670 | 653 | """ | ||
671 | 654 | now = extime.Time() | ||
672 | 655 | self.grabber.now = lambda: now | ||
673 | 656 | self.grabber.markSuccess(b'123abc', StubMessage()) | ||
674 | 657 | [pop3uid] = list(self.store.query( | ||
675 | 658 | grabber.POP3UID, grabber.POP3UID.value == b'123abc')) | ||
676 | 659 | self.assertEqual(now, pop3uid.retrieved) | ||
677 | 660 | |||
678 | 661 | |||
679 | 662 | def test_failureTimestamp(self): | ||
680 | 663 | """ | ||
681 | 664 | The L{POP3UID} instance created by L{POP3Grabber.markFailure} has its | ||
682 | 665 | C{retrieved} attribute set to the current time as reported by | ||
683 | 666 | L{POP3Grabber.now}. | ||
684 | 667 | """ | ||
685 | 668 | now = extime.Time() | ||
686 | 669 | self.grabber.now = lambda: now | ||
687 | 670 | self.grabber.markFailure(b'123abc', object()) | ||
688 | 671 | [pop3uid] = list(self.store.query( | ||
689 | 672 | grabber.POP3UID, grabber.POP3UID.value == b'123abc')) | ||
690 | 673 | self.assertEqual(now, pop3uid.retrieved) | ||
691 | 674 | |||
692 | 675 | |||
693 | 544 | def test_delete(self): | 676 | def test_delete(self): |
694 | 545 | """ | 677 | """ |
695 | 546 | L{POP3Grabber.delete} unschedules the grabber. | 678 | L{POP3Grabber.delete} unschedules the grabber. |
696 | @@ -553,3 +685,213 @@ | |||
697 | 553 | # was scheduled either. | 685 | # was scheduled either. |
698 | 554 | self.assertEqual( | 686 | self.assertEqual( |
699 | 555 | [], list(store.query(scheduler.TimedEvent))) | 687 | [], list(store.query(scheduler.TimedEvent))) |
700 | 688 | |||
701 | 689 | |||
702 | 690 | def test_shouldDeleteOldMessage(self): | ||
703 | 691 | """ | ||
704 | 692 | C{shouldDelete} accepts a list of (index, uid) pairs and returns a list | ||
705 | 693 | of (index, uid) pairs corresponding to messages which may now be deleted | ||
706 | 694 | from the POP3 server (due to having been downloaded more than a fixed | ||
707 | 695 | number of days in the past). | ||
708 | 696 | """ | ||
709 | 697 | epoch = extime.Time() | ||
710 | 698 | now = epoch - (self.grabber.DELETE_DELAY + timedelta(days=1)) | ||
711 | 699 | |||
712 | 700 | self.grabber.now = lambda: now | ||
713 | 701 | |||
714 | 702 | # Generate some state representing a past success | ||
715 | 703 | oldEnough = b'123abc' | ||
716 | 704 | self.grabber.markSuccess(oldEnough, StubMessage()) | ||
717 | 705 | |||
718 | 706 | # Wind the clock forward far enough so that oldEnough should be | ||
719 | 707 | # considered old enough for deletion. | ||
720 | 708 | now = epoch | ||
721 | 709 | |||
722 | 710 | self.assertEqual( | ||
723 | 711 | [(3, oldEnough)], self.grabber.shouldDelete([(3, oldEnough)])) | ||
724 | 712 | |||
725 | 713 | |||
726 | 714 | def test_shouldDeleteOtherGrabberState(self): | ||
727 | 715 | """ | ||
728 | 716 | Messages downloaded by an unrelated grabber are not considered by | ||
729 | 717 | C{shouldDelete}. | ||
730 | 718 | """ | ||
731 | 719 | uid = b'abcdef' | ||
732 | 720 | then = extime.Time() - self.grabber.DELETE_DELAY - timedelta(days=1) | ||
733 | 721 | grabber.POP3UID( | ||
734 | 722 | store=self.store, grabberID=u'bob@example.org:default', value=uid, | ||
735 | 723 | retrieved=then) | ||
736 | 724 | |||
737 | 725 | self.assertEqual([], self.grabber.shouldDelete([(5, uid)])) | ||
738 | 726 | |||
739 | 727 | |||
740 | 728 | |||
741 | 729 | def test_shouldDeleteNewMessage(self): | ||
742 | 730 | """ | ||
743 | 731 | Messages downloaded less than a fixed number of days in the past are not | ||
744 | 732 | indicated as deletable by C{shouldDelete}. | ||
745 | 733 | """ | ||
746 | 734 | epoch = extime.Time() | ||
747 | 735 | now = epoch - (self.grabber.DELETE_DELAY - timedelta(days=1)) | ||
748 | 736 | |||
749 | 737 | self.grabber.now = lambda: now | ||
750 | 738 | |||
751 | 739 | # Generate some state representing a *recently* past success | ||
752 | 740 | newEnough = b'xyz123' | ||
753 | 741 | self.grabber.markSuccess(newEnough, StubMessage()) | ||
754 | 742 | |||
755 | 743 | # Wind the clock forward, but not so far forward that newEnough is | ||
756 | 744 | # considered old enough for deletion. | ||
757 | 745 | now = epoch | ||
758 | 746 | |||
759 | 747 | self.assertEqual( | ||
760 | 748 | [], self.grabber.shouldDelete([(5, newEnough)])) | ||
761 | 749 | |||
762 | 750 | |||
763 | 751 | def test_shouldDeleteFailedMessage(self): | ||
764 | 752 | """ | ||
765 | 753 | Messages for which the download failed are not indicated as deletable by | ||
766 | 754 | C{shouldDelete}. | ||
767 | 755 | """ | ||
768 | 756 | epoch = extime.Time() | ||
769 | 757 | now = epoch - (self.grabber.DELETE_DELAY + timedelta(days=1)) | ||
770 | 758 | |||
771 | 759 | self.grabber.now = lambda: now | ||
772 | 760 | |||
773 | 761 | # Generate some state representing a past failure | ||
774 | 762 | failed = b'xyz123' | ||
775 | 763 | self.grabber.markFailure(failed, object()) | ||
776 | 764 | |||
777 | 765 | # Wind the clock forward enough so that the failed message would be old | ||
778 | 766 | # enough - if it had been a success. | ||
779 | 767 | now = epoch | ||
780 | 768 | |||
781 | 769 | self.assertEqual( | ||
782 | 770 | [], self.grabber.shouldDelete([(7, failed)])) | ||
783 | 771 | |||
784 | 772 | |||
785 | 773 | def test_shouldDeleteUnknownMessage(self): | ||
786 | 774 | """ | ||
787 | 775 | Messages which have not been downloaded are not indicated as deletable | ||
788 | 776 | by C{shouldDelete}. | ||
789 | 777 | """ | ||
790 | 778 | self.assertEqual( | ||
791 | 779 | [], self.grabber.shouldDelete([(7, b'9876wxyz')])) | ||
792 | 780 | |||
793 | 781 | |||
794 | 782 | def test_shouldDeleteMessageLimit(self): | ||
795 | 783 | """ | ||
796 | 784 | At most around 1000 (the exact value of the limit will be imposed by | ||
797 | 785 | SQLite3) messages are considered for deletion by C{shouldDelete}. | ||
798 | 786 | """ | ||
799 | 787 | epoch = extime.Time() | ||
800 | 788 | now = epoch - (self.grabber.DELETE_DELAY + timedelta(days=1)) | ||
801 | 789 | self.grabber.now = lambda: now | ||
802 | 790 | |||
803 | 791 | uidList = [] | ||
804 | 792 | for i in range(1100): | ||
805 | 793 | uid = b'%dabc' % (i,) | ||
806 | 794 | uidList.append((i, uid)) | ||
807 | 795 | self.grabber.markSuccess(uid, StubMessage()) | ||
808 | 796 | |||
809 | 797 | # Spin the clock forward so all those messages are considered deletable. | ||
810 | 798 | now = epoch | ||
811 | 799 | self.assertEqual( | ||
812 | 800 | uidList[:996], self.grabber.shouldDelete(uidList)) | ||
813 | 801 | |||
814 | 802 | |||
815 | 803 | def test_now(self): | ||
816 | 804 | """ | ||
817 | 805 | L{POP3Grabber.now} returns the current time. | ||
818 | 806 | """ | ||
819 | 807 | self.assertTrue(extime.Time() <= self.grabber.now()) | ||
820 | 808 | self.assertTrue(self.grabber.now() <= extime.Time()) | ||
821 | 809 | |||
822 | 810 | |||
823 | 811 | def test_markDeleted(self): | ||
824 | 812 | """ | ||
825 | 813 | L{POP3Grabber.markDeleted} deletes the L{POP3UID} corresponding to the | ||
826 | 814 | message UID passed in. | ||
827 | 815 | """ | ||
828 | 816 | uid = b'abcdef' | ||
829 | 817 | self.grabber.markSuccess(uid, StubMessage()) | ||
830 | 818 | self.grabber.markDeleted(uid) | ||
831 | 819 | persistentUIDs = list(self.store.query( | ||
832 | 820 | grabber.POP3UID, grabber.POP3UID.value == uid)) | ||
833 | 821 | self.assertEqual([], persistentUIDs) | ||
834 | 822 | |||
835 | 823 | |||
836 | 824 | def test_markDeletedOtherGrabber(self): | ||
837 | 825 | """ | ||
838 | 826 | L{POP3Grabber.markDeleted} does not delete a L{POP3UID} with a matching | ||
839 | 827 | message UID but which belongs to a different grabber. | ||
840 | 828 | """ | ||
841 | 829 | uid = b'abcdef' | ||
842 | 830 | pop3uid = grabber.POP3UID( | ||
843 | 831 | store=self.store, | ||
844 | 832 | grabberID=u'bob@example.org:default', | ||
845 | 833 | value=uid, | ||
846 | 834 | retrieved=extime.Time()) | ||
847 | 835 | self.grabber.markDeleted(uid) | ||
848 | 836 | persistentUIDs = list(self.store.query( | ||
849 | 837 | grabber.POP3UID, grabber.POP3UID.value == uid)) | ||
850 | 838 | self.assertEqual([pop3uid], persistentUIDs) | ||
851 | 839 | |||
852 | 840 | |||
853 | 841 | |||
854 | 842 | class ShouldDeleteComplexityTests(unittest.TestCase): | ||
855 | 843 | """ | ||
856 | 844 | Tests for the query complexity of L{POP3Grabber.shouldDelete}. | ||
857 | 845 | """ | ||
858 | 846 | def test_otherGrabber(self): | ||
859 | 847 | """ | ||
860 | 848 | The database complexity of L{POP3Grabber.shouldDelete} is independent of | ||
861 | 849 | the number of L{POP3UID} items which belong to another L{POP3Grabber}. | ||
862 | 850 | """ | ||
863 | 851 | self._complexityTest( | ||
864 | 852 | lambda grabberItem: grabber.POP3UID( | ||
865 | 853 | store=grabberItem.store, retrieved=extime.Time(), failed=False, | ||
866 | 854 | grabberID=grabberItem.grabberID + b'unrelated', value=b'123')) | ||
867 | 855 | |||
868 | 856 | |||
869 | 857 | def test_shouldNotDelete(self): | ||
870 | 858 | """ | ||
871 | 859 | The database complexity of L{POP3Grabber.shouldDelete} is independent of | ||
872 | 860 | the number of L{POP3UID} items which exist in the database but do not | ||
873 | 861 | yet merit deletion. | ||
874 | 862 | """ | ||
875 | 863 | self._complexityTest( | ||
876 | 864 | lambda grabberItem: grabber.POP3UID( | ||
877 | 865 | store=grabberItem.store, retrieved=extime.Time(), failed=False, | ||
878 | 866 | grabberID=grabberItem.grabberID, value=b'def')) | ||
879 | 867 | |||
880 | 868 | |||
881 | 869 | def _complexityTest(self, makePOP3UID): | ||
882 | 870 | s = store.Store() | ||
883 | 871 | counter = QueryCounter(s) | ||
884 | 872 | |||
885 | 873 | config = grabber.GrabberConfiguration(store=s) | ||
886 | 874 | grabberItem = grabber.POP3Grabber( | ||
887 | 875 | store=s, | ||
888 | 876 | config=config, | ||
889 | 877 | username=u"testuser", | ||
890 | 878 | domain=u"example.com", | ||
891 | 879 | password=u"password") | ||
892 | 880 | |||
893 | 881 | # Create at least one POP3UID, since zero-items-in-table is always | ||
894 | 882 | # different from any-items-in-table. | ||
895 | 883 | for i in range(5): | ||
896 | 884 | grabber.POP3UID( | ||
897 | 885 | store=s, retrieved=extime.Time(), failed=False, | ||
898 | 886 | grabberID=grabberItem.grabberID, value=b'abc' + str(i)) | ||
899 | 887 | |||
900 | 888 | fewer = counter.measure( | ||
901 | 889 | lambda: grabberItem.shouldDelete([b"123"])) | ||
902 | 890 | |||
903 | 891 | # Create another non-matching POP3UID | ||
904 | 892 | makePOP3UID(grabberItem) | ||
905 | 893 | |||
906 | 894 | more = counter.measure( | ||
907 | 895 | lambda: grabberItem.shouldDelete([b"123"])) | ||
908 | 896 | |||
909 | 897 | self.assertEqual(fewer, more) |
Some things left to do:
- Tweak the code that decides the maximum number of messages to delete in a single session
- Synchronize the Axiom POP3UID deletion transaction(s) with the POP3 server's message TRANSACTION state. A lost connection (eg, from server shutdown) during deletion leaves the messages on the server but erases Quotient's notion of them, causing them to be re-downloaded.
- Reconsider the week long deletion moratorium. Perhaps a useful feature, but *I* don't actually need it. Maybe just set it much lower for now - perhaps to an hour. A week of mail is still over 3000 messages in my mailbox.