~ubuntu-branches/ubuntu/natty/miro/natty

« back to all changes in this revision

Viewing changes to portable/test/feedtest.py

  • Committer: Bazaar Package Importer
  • Author(s): Bryce Harrington
  • Date: 2011-01-22 02:46:33 UTC
  • mfrom: (1.4.10 upstream) (1.7.5 experimental)
  • Revision ID: james.westby@ubuntu.com-20110122024633-kjme8u93y2il5nmf
Tags: 3.5.1-1ubuntu1
* Merge from debian.  Remaining ubuntu changes:
  - Use python 2.7 instead of python 2.6
  - Relax dependency on python-dbus to >= 0.83.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
import os
2
 
import unittest
3
 
from tempfile import mkstemp, gettempdir
4
 
from datetime import datetime
5
 
from time import sleep
6
 
 
7
 
from miro import config
8
 
from miro import feedparser
9
 
from miro import prefs
10
 
from miro import dialogs
11
 
from miro import database
12
 
from miro import storedatabase
13
 
from miro import feedparserutil
14
 
from miro.plat import resources
15
 
from miro.item import Item
16
 
from miro.feed import validate_feed_url, normalize_feed_url, Feed
17
 
 
18
 
from miro.test.framework import MiroTestCase, EventLoopTest
19
 
 
20
 
class FakeDownloader:
21
 
    pass
22
 
 
23
 
class AcceptScrapeTestDelegate:
24
 
    def __init__(self):
25
 
        self.calls = 0
26
 
 
27
 
    def run_dialog(self, dialog):
28
 
        self.calls += 1
29
 
        if not isinstance(dialog, dialogs.ChoiceDialog):
30
 
            raise AssertionError("Only expected ChoiceDialogs")
31
 
        if not dialog.title.startswith("Channel is not compatible"):
32
 
            raise AssertionError("Only expected scrape dialogs")
33
 
        dialog.choice = dialogs.BUTTON_YES
34
 
        dialog.callback(dialog)
35
 
 
36
 
class FeedURLValidationTest(MiroTestCase):
37
 
    def test_positive(self):
38
 
        for testurl in [u"http://foo.bar.com/",
39
 
                        u"https://foo.bar.com/",
40
 
                        ]:
41
 
            self.assertEqual(validate_feed_url(testurl), True)
42
 
 
43
 
    def test_negative(self):
44
 
        for testurl in [u"feed://foo.bar.com/",
45
 
                        u"http://foo.bar.com",
46
 
                        u"http:foo.bar.com/",
47
 
                        u"https:foo.bar.com/",
48
 
                        u"feed:foo.bar.com/",
49
 
                        u"http:/foo.bar.com/",
50
 
                        u"https:/foo.bar.com/",
51
 
                        u"feed:/foo.bar.com/",
52
 
                        u"http:///foo.bar.com/",
53
 
                        u"https:///foo.bar.com/",
54
 
                        u"feed:///foo.bar.com/",
55
 
                        u"foo.bar.com",
56
 
                        u"crap:foo.bar.com",
57
 
                        u"crap:/foo.bar.com",
58
 
                        u"crap://foo.bar.com",
59
 
                        u"crap:///foo.bar.com",
60
 
                        ]:
61
 
            self.assertEqual(validate_feed_url(testurl), False)
62
 
 
63
 
        # FIXME - add tests for all the other kinds of urls that
64
 
        # validate_feed_url handles.
65
 
 
66
 
class FeedURLNormalizationTest(MiroTestCase):
67
 
    def test_easy(self):
68
 
        for i, o in [(u"http://foo.bar.com", u"http://foo.bar.com/"),
69
 
                     (u"https://foo.bar.com", u"https://foo.bar.com/"),
70
 
                     (u"feed://foo.bar.com", u"http://foo.bar.com/")
71
 
                     ]:
72
 
            self.assertEqual(normalize_feed_url(i), o)
73
 
 
74
 
    def test_garbage(self):
75
 
        for i, o in [(u"http:foo.bar.com", u"http://foo.bar.com/"),
76
 
                     (u"https:foo.bar.com", u"https://foo.bar.com/"),
77
 
                     (u"feed:foo.bar.com", u"http://foo.bar.com/"),
78
 
                     (u"http:/foo.bar.com", u"http://foo.bar.com/"),
79
 
                     (u"https:/foo.bar.com", u"https://foo.bar.com/"),
80
 
                     (u"feed:/foo.bar.com", u"http://foo.bar.com/"),
81
 
                     (u"http:///foo.bar.com", u"http://foo.bar.com/"),
82
 
                     (u"https:///foo.bar.com", u"https://foo.bar.com/"),
83
 
                     (u"feed:///foo.bar.com", u"http://foo.bar.com/"),
84
 
                     (u"foo.bar.com", u"http://foo.bar.com/"),
85
 
                     (u"http://foo.bar.com:80", u"http://foo.bar.com:80/"),
86
 
                     ]:
87
 
            self.assertEquals(normalize_feed_url(i), o)
88
 
 
89
 
        # FIXME - add tests for all the other kinds of feeds that
90
 
        # normalize_feed_url handles.
91
 
 
92
 
class FeedTestCase(EventLoopTest):
93
 
    def setUp(self):
94
 
        EventLoopTest.setUp(self)
95
 
        self.filename = self.make_temp_path()
96
 
 
97
 
    def write_file(self, content):
98
 
        self.url = u'file://%s' % self.filename
99
 
        handle = file(self.filename,"wb")
100
 
        # RSS 2.0 example feed
101
 
        # http://cyber.law.harvard.edu/blogs/gems/tech/rss2sample.xml
102
 
        handle.write(content)
103
 
        handle.close()
104
 
 
105
 
    def update_feed(self, feed):
106
 
        feed.update()
107
 
        self.processThreads()
108
 
        self.processIdles()
109
 
 
110
 
    def make_feed(self):
111
 
        feed = Feed(self.url)
112
 
        self.update_feed(feed)
113
 
        return feed
114
 
 
115
 
class SimpleFeedTestCase(FeedTestCase):
116
 
    def setUp(self):
117
 
        FeedTestCase.setUp(self)
118
 
        # Based on 
119
 
        # http://cyber.law.harvard.edu/blogs/gems/tech/rss2sample.xml
120
 
 
121
 
        # this rss feed has no enclosures.
122
 
        self.write_file("""<?xml version="1.0"?>
123
 
<rss version="2.0">
124
 
   <channel>
125
 
      <title>Liftoff News</title>
126
 
      <link>http://liftoff.msfc.nasa.gov/</link>
127
 
      <description>Liftoff to Space Exploration.</description>
128
 
      <language>en-us</language>
129
 
      <pubDate>Tue, 10 Jun 2003 04:00:00 GMT</pubDate>
130
 
 
131
 
      <lastBuildDate>Tue, 10 Jun 2003 09:41:01 GMT</lastBuildDate>
132
 
      <docs>http://blogs.law.harvard.edu/tech/rss</docs>
133
 
      <generator>Weblog Editor 2.0</generator>
134
 
      <managingEditor>editor@example.com</managingEditor>
135
 
      <webMaster>webmaster@example.com</webMaster>
136
 
      <item>
137
 
         <title>Star City</title>
138
 
         <link>http://liftoff.msfc.nasa.gov/news/2003/news-starcity.mov</link>
139
 
         <description>How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's &lt;a href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm"&gt;Star City&lt;/a&gt;.</description>
140
 
         <pubDate>Tue, 03 Jun 2003 09:39:21 GMT</pubDate>
141
 
         <guid>http://liftoff.msfc.nasa.gov/2003/06/03.html#item573</guid>
142
 
      </item>
143
 
      <item>
144
 
         <description>Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a &lt;a href="http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm"&gt;partial eclipse of the Sun&lt;/a&gt; on Saturday, May 31st.</description>
145
 
         <pubDate>Fri, 30 May 2003 11:06:42 GMT</pubDate>
146
 
         <guid>http://liftoff.msfc.nasa.gov/2003/05/30.html#item572</guid>
147
 
      </item>
148
 
      <item>
149
 
         <title>The Engine That Does More</title>
150
 
         <link>http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp</link>
151
 
         <description>Before man travels to Mars, NASA hopes to design new engines that will let us fly through the Solar System more quickly.  The proposed VASIMR engine would do that.</description>
152
 
         <pubDate>Tue, 27 May 2003 08:37:32 GMT</pubDate>
153
 
         <guid>http://liftoff.msfc.nasa.gov/2003/05/27.html#item571</guid>
154
 
      </item>
155
 
      <item>
156
 
         <title>Astronauts' Dirty Laundry</title>
157
 
         <link>http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp</link>
158
 
         <description>Compared to earlier spacecraft, the International Space Station has many luxuries, but laundry facilities are not one of them.  Instead, astronauts have other options.</description>
159
 
         <pubDate>Tue, 20 May 2003 08:56:02 GMT</pubDate>
160
 
         <guid>http://liftoff.msfc.nasa.gov/2003/05/20.html#item570</guid>
161
 
      </item>
162
 
   </channel>
163
 
</rss>""")
164
 
 
165
 
    def test_run(self):
166
 
        dialogs.delegate = AcceptScrapeTestDelegate()
167
 
        my_feed = self.make_feed()
168
 
 
169
 
        # the feed has no enclosures, but we now insert enclosures into it.
170
 
        # thus it should not cause a dialog to pop up and ask the user if they
171
 
        # want to scrape.
172
 
        self.assertEqual(dialogs.delegate.calls, 0)
173
 
        # the Feed, plus the 1 item that is a video
174
 
        items = list(Item.make_view())
175
 
        self.assertEqual(len(items), 1)
176
 
 
177
 
        # make sure that re-updating doesn't re-create the items
178
 
        my_feed.update()
179
 
        items = list(Item.make_view())
180
 
        self.assertEqual(len(items), 1)
181
 
        my_feed.remove()
182
 
 
183
 
class MultiFeedExpireTest(FeedTestCase):
184
 
    def write_files(self, subfeed_count, feed_item_count):
185
 
        all_urls = []
186
 
        self.filenames = []
187
 
 
188
 
        content = self.make_feed_content(feed_item_count)
189
 
        for i in xrange(subfeed_count):
190
 
            filename = self.make_temp_path()
191
 
            open(filename, 'wb').write(content)
192
 
            all_urls.append(u"file://%s" % filename)
193
 
            self.filenames.append(filename)
194
 
 
195
 
        self.url = u'dtv:multi:' + ','.join(all_urls) + "," + 'testquery'
196
 
 
197
 
    def rewrite_files(self, feed_item_count):
198
 
        content = self.make_feed_content(feed_item_count)
199
 
        for filename in self.filenames:
200
 
            open(filename, 'wb').write(content)
201
 
 
202
 
    def make_feed_content(self, entry_count):
203
 
        # make a feed with a new item and parse it
204
 
        items = []
205
 
        counter = 0
206
 
 
207
 
        items.append("""<?xml version="1.0"?>
208
 
<rss version="2.0">
209
 
   <channel>
210
 
      <title>Downhill Battle Pics</title>
211
 
      <link>http://downhillbattle.org/</link>
212
 
      <description>Downhill Battle is a non-profit organization working to support participatory culture and build a fairer music industry.</description>
213
 
      <pubDate>Wed, 16 Mar 2005 12:03:42 EST</pubDate>
214
 
""")
215
 
 
216
 
        for x in range(entry_count):
217
 
            counter += 1
218
 
            items.append("""\
219
 
<item>
220
 
 <title>Bumper Sticker</title>
221
 
 <guid>guid-%s</guid>
222
 
 <enclosure url="http://downhillbattle.org/key/gallery/%s.mpg" />
223
 
 <description>I'm a musician and I support filesharing.</description>
224
 
</item>
225
 
""" % (counter, counter))
226
 
 
227
 
        items.append("""
228
 
   </channel>
229
 
</rss>""")
230
 
        return "".join(items)
231
 
 
232
 
    def test_multi_feed_expire(self):
233
 
        # test what happens when a RSSMultiFeed has feeds that
234
 
        # reference the same item, and they are truncated at the same
235
 
        # time (#11756)
236
 
 
237
 
        self.write_files(5, 10) # 5 feeds containing 10 identical items
238
 
        self.feed = self.make_feed()
239
 
        config.set(prefs.TRUNCATE_CHANNEL_AFTER_X_ITEMS, 4)
240
 
        config.set(prefs.MAX_OLD_ITEMS_DEFAULT, 5)
241
 
        self.rewrite_files(1) # now only 5 items in each feed
242
 
        self.update_feed(self.feed)
243
 
 
244
 
class EnclosureFeedTestCase(FeedTestCase):
245
 
    def setUp(self):
246
 
        FeedTestCase.setUp(self)
247
 
        self.write_file("""<?xml version="1.0"?>
248
 
<rss version="2.0">
249
 
   <channel>
250
 
      <title>Downhill Battle Pics</title>
251
 
      <link>http://downhillbattle.org/</link>
252
 
      <description>Downhill Battle is a non-profit organization working to support participatory culture and build a fairer music industry.</description>
253
 
      <pubDate>Wed, 16 Mar 2005 12:03:42 EST</pubDate>
254
 
      <item>
255
 
         <title>Bumper Sticker</title>
256
 
         <enclosure url="http://downhillbattle.org/key/gallery/chriscA.mpg" />
257
 
         <description>I'm a musician and I support filesharing.</description>
258
 
 
259
 
      </item>
260
 
      <item>
261
 
         <title>T-shirt</title>
262
 
         <enclosure url="http://downhillbattle.org/key/gallery/payola_tshirt.mpg" />
263
 
      </item>
264
 
      <item>
265
 
         <enclosure url="http://downhillbattle.org/key/gallery/chriscE.mpg" />
266
 
         <description>Flyer in Yucaipa, CA</description>
267
 
      </item>
268
 
      <item>
269
 
         <enclosure url="http://downhillbattle.org/key/gallery/jalabel_nov28.mpg" />
270
 
      </item>
271
 
      <item>
272
 
         <enclosure url="http://downhillbattle.org/key/gallery/jalabel_nov28.jpg" />
273
 
      </item>
274
 
      
275
 
   </channel>
276
 
</rss>""")
277
 
 
278
 
    def test_run(self):
279
 
        my_feed = self.make_feed()
280
 
        items = list(Item.make_view())
281
 
        self.assertEqual(len(items), 4)
282
 
        # make sure that re-updating doesn't re-create the items
283
 
        my_feed.update()
284
 
        items = list(Item.make_view())
285
 
        self.assertEqual(len(items), 4)
286
 
        my_feed.remove()
287
 
 
288
 
class OldItemExpireTest(FeedTestCase):
289
 
    # Test that old items expire when the feed gets too big
290
 
    def setUp(self):
291
 
        FeedTestCase.setUp(self)
292
 
        self.counter = 0
293
 
        self.write_new_feed()
294
 
        self.feed = self.make_feed()
295
 
        config.set(prefs.TRUNCATE_CHANNEL_AFTER_X_ITEMS, 4)
296
 
        config.set(prefs.MAX_OLD_ITEMS_DEFAULT, 20)
297
 
 
298
 
    def write_new_feed(self, entryCount=2):
299
 
        # make a feed with a new item and parse it
300
 
        items = []
301
 
 
302
 
        items.append("""<?xml version="1.0"?>
303
 
<rss version="2.0">
304
 
   <channel>
305
 
      <title>Downhill Battle Pics</title>
306
 
      <link>http://downhillbattle.org/</link>
307
 
      <description>Downhill Battle is a non-profit organization working to support participatory culture and build a fairer music industry.</description>
308
 
      <pubDate>Wed, 16 Mar 2005 12:03:42 EST</pubDate>
309
 
""")
310
 
 
311
 
        for x in range(entryCount):
312
 
            self.counter += 1
313
 
            items.append("""\
314
 
<item>
315
 
 <title>Bumper Sticker</title>
316
 
 <guid>guid-%s</guid>
317
 
 <enclosure url="http://downhillbattle.org/key/gallery/%s.mpg" />
318
 
 <description>I'm a musician and I support filesharing.</description>
319
 
</item>
320
 
""" % (self.counter, self.counter))
321
 
 
322
 
        items.append("""
323
 
   </channel>
324
 
</rss>""")
325
 
        self.write_file("\n".join(items))
326
 
 
327
 
    def check_guids(self, *ids):
328
 
        actual = set()
329
 
        for i in Item.make_view():
330
 
            actual.add(i.get_rss_id())
331
 
        correct = set(['guid-%d' % i for i in ids])
332
 
        self.assertEquals(actual, correct)
333
 
 
334
 
    def parse_new_feed(self, entryCount=2):
335
 
        self.write_new_feed(entryCount)
336
 
        self.update_feed(self.feed)
337
 
 
338
 
    def test_simple_overflow(self):
339
 
        self.assertEqual(Item.make_view().count(), 2)
340
 
        self.parse_new_feed()
341
 
        self.assertEqual(Item.make_view().count(), 4)
342
 
        self.parse_new_feed()
343
 
        self.assertEqual(Item.make_view().count(), 4)
344
 
        self.check_guids(3, 4, 5, 6)
345
 
 
346
 
    def test_overflow_with_downloads(self):
347
 
        items = list(Item.make_view())
348
 
        items[0]._downloader = FakeDownloader()
349
 
        items[1]._downloader = FakeDownloader()
350
 
        self.assertEqual(len(items), 2)
351
 
        self.parse_new_feed()
352
 
        self.parse_new_feed()
353
 
        self.check_guids(1, 2, 5, 6)
354
 
 
355
 
    def test_overflow_still_in_feed(self):
356
 
        config.set(prefs.TRUNCATE_CHANNEL_AFTER_X_ITEMS, 0)
357
 
        self.parse_new_feed(6)
358
 
        self.check_guids(3, 4, 5, 6, 7, 8)
359
 
 
360
 
    def test_overflow_with_replacement(self):
361
 
        # Keep item with guid-2 in the feed.
362
 
        config.set(prefs.TRUNCATE_CHANNEL_AFTER_X_ITEMS, 0)
363
 
        self.counter = 1
364
 
        self.parse_new_feed(5)
365
 
        self.check_guids(2, 3, 4, 5, 6)
366
 
 
367
 
    def test_overflow_with_max_old_items(self):
368
 
        config.set(prefs.TRUNCATE_CHANNEL_AFTER_X_ITEMS, 1000) # don't bother
369
 
        self.assertEqual(Item.make_view().count(), 2)
370
 
        self.parse_new_feed()
371
 
        self.assertEquals(Item.make_view().count(), 4)
372
 
        self.parse_new_feed()
373
 
        self.feed.setMaxOldItems(4)
374
 
        self.feed.actualFeed.clean_old_items()
375
 
        while self.feed.actualFeed.updating:
376
 
            self.processThreads()
377
 
            self.processIdles()
378
 
            sleep(0.1)
379
 
        self.assertEquals(Item.make_view().count(), 6)            
380
 
        self.feed.setMaxOldItems(2)
381
 
        self.feed.actualFeed.clean_old_items()
382
 
        while self.feed.actualFeed.updating:
383
 
            self.processThreads()
384
 
            self.processIdles()
385
 
            sleep(0.1)
386
 
        self.assertEquals(Item.make_view().count(), 4)
387
 
        self.check_guids(3, 4, 5, 6)
388
 
 
389
 
    def test_overflow_with_global_max_old_items(self):
390
 
        config.set(prefs.TRUNCATE_CHANNEL_AFTER_X_ITEMS, 1000) # don't bother
391
 
        self.assertEqual(Item.make_view().count(), 2)
392
 
        self.parse_new_feed()
393
 
        self.assertEquals(Item.make_view().count(), 4)
394
 
        self.parse_new_feed()
395
 
        config.set(prefs.MAX_OLD_ITEMS_DEFAULT, 4)
396
 
        self.feed.actualFeed.clean_old_items()
397
 
        while self.feed.actualFeed.updating:
398
 
            self.processThreads()
399
 
            self.processIdles()
400
 
            sleep(0.1)
401
 
        self.assertEquals(Item.make_view().count(), 6)
402
 
        config.set(prefs.MAX_OLD_ITEMS_DEFAULT, 2)
403
 
        self.feed.actualFeed.clean_old_items()
404
 
        while self.feed.actualFeed.updating:
405
 
            self.processThreads()
406
 
            self.processIdles()
407
 
            sleep(0.1)
408
 
        self.assertEquals(Item.make_view().count(), 4)
409
 
        self.check_guids(3, 4, 5, 6)
410
 
 
411
 
class FeedParserAttributesTestCase(FeedTestCase):
412
 
    """Test that we save/restore attributes from feedparser correctly.
413
 
 
414
 
    We don't store feedparser dicts in the database.  This test case is
415
 
    checking if the values we to the database are the same as the original
416
 
    ones from feedparser.
417
 
    """
418
 
    def setUp(self):
419
 
        FeedTestCase.setUp(self)
420
 
        self.tempdb = os.path.join(gettempdir(), 'democracy-temp-db')
421
 
        if os.path.exists(self.tempdb):
422
 
            os.remove(self.tempdb)
423
 
        self.reload_database(self.tempdb)
424
 
        self.write_feed()
425
 
        self.parsed_feed = feedparser.parse(self.filename)
426
 
        self.make_feed()
427
 
        self.save_then_restore_db()
428
 
 
429
 
    def tearDown(self):
430
 
        self.runPendingIdles()
431
 
        self.shutdown_database()
432
 
        os.remove(self.tempdb)
433
 
        FeedTestCase.tearDown(self)
434
 
 
435
 
    def save_then_restore_db(self):
436
 
        self.reload_database(self.tempdb)
437
 
        self.feed = Feed.make_view().get_singleton()
438
 
        self.item = Item.make_view().get_singleton()
439
 
 
440
 
    def write_feed(self):
441
 
        self.write_file("""<?xml version="1.0"?>
442
 
<rss version="2.0">
443
 
    <channel>
444
 
        <title>Downhill Battle Pics</title>
445
 
        <link>http://downhillbattle.org/</link>
446
 
        <description>Downhill Battle is a non-profit organization working to support participatory culture and build a fairer music industry.</description>
447
 
        <pubDate>Wed, 16 Mar 2005 12:03:42 EST</pubDate>
448
 
 
449
 
        <item>
450
 
            <title>Bumper Sticker</title>
451
 
            <link>http://downhillbattle.org/item</link>
452
 
            <comments>http://downhillbattle.org/item/comments</comments>
453
 
            <creativeCommons:license>http://www.creativecommons.org/licenses/by-nd/1.0</creativeCommons:license>
454
 
            <guid>guid-1234</guid>
455
 
            <enclosure url="http://downhillbattle.org/key/gallery/movie.mpg"
456
 
                length="1234"
457
 
                type="video/mpeg"
458
 
                />
459
 
            <description>I'm a musician and I support filesharing.</description>
460
 
            <pubDate>Fri, 18 Mar 2005 12:03:42 EST</pubDate>
461
 
            <media:thumbnail url="%(thumburl)s" />
462
 
            <dtv:paymentlink url="http://www.example.com/payment.html" />
463
 
        </item>
464
 
 
465
 
    </channel>
466
 
</rss>
467
 
""" % {'thumburl': resources.url("testdata/democracy-now-unicode-bug.xml")})
468
 
 
469
 
 
470
 
    def test_attributes(self):
471
 
        entry = self.parsed_feed.entries[0]
472
 
        self.assertEquals(self.item.get_rss_id(), entry.id)
473
 
        self.assertEquals(self.item.get_thumbnail_url(), entry.thumbnail['url'])
474
 
        self.assertEquals(self.item.get_title(), entry.title)
475
 
        self.assertEquals(self.item.get_raw_description(), entry.description)
476
 
        self.assertEquals(self.item.get_link(), entry.link)
477
 
        self.assertEquals(self.item.get_payment_link(), entry.payment_url)
478
 
        self.assertEquals(self.item.get_license(), entry.license)
479
 
        self.assertEquals(self.item.get_comments_link(), entry.comments)
480
 
 
481
 
        enclosure = entry.enclosures[0]
482
 
        self.assertEquals(self.item.get_url(), enclosure.url)
483
 
        self.assertEquals(self.item.get_size(), int(enclosure.length))
484
 
        self.assertEquals(self.item.get_format(), '.mpeg')
485
 
 
486
 
    def test_remove_rssid(self):
487
 
        self.item.remove_rss_id()
488
 
        self.save_then_restore_db()
489
 
        self.assertEquals(self.item.get_rss_id(), None)
490
 
 
491
 
    def test_change_title(self):
492
 
        entry = self.parsed_feed.entries[0]
493
 
        self.item.set_title(u"new title")
494
 
        self.save_then_restore_db()
495
 
        self.assertEquals(self.item.get_title(), "new title")
496
 
 
497
 
if __name__ == "__main__":
498
 
    unittest.main()