Code

20bdafbab6dd08c3a759903ff59b90b3f5191ce8
[roundup.git] / test / db_test_base.py
1 #
2 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
3 # This module is free software, and you may redistribute it and/or modify
4 # under the same terms as Python, so long as this copyright message and
5 # disclaimer are retained in their original form.
6 #
7 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
8 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
9 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
10 # POSSIBILITY OF SUCH DAMAGE.
11 #
12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
14 # FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17 #
18 # $Id: db_test_base.py,v 1.101 2008-08-19 01:40:59 richard Exp $
20 import unittest, os, shutil, errno, imp, sys, time, pprint, sets, base64, os.path
22 from roundup.hyperdb import String, Password, Link, Multilink, Date, \
23     Interval, DatabaseError, Boolean, Number, Node
24 from roundup.mailer import Mailer
25 from roundup import date, password, init, instance, configuration, support
27 from mocknull import MockNull
29 config = configuration.CoreConfig()
30 config.DATABASE = "db"
31 config.RDBMS_NAME = "rounduptest"
32 config.RDBMS_HOST = "localhost"
33 config.RDBMS_USER = "rounduptest"
34 config.RDBMS_PASSWORD = "rounduptest"
35 #config.logging = MockNull()
36 # these TRACKER_WEB and MAIL_DOMAIN values are used in mailgw tests
37 config.MAIL_DOMAIN = "your.tracker.email.domain.example"
38 config.TRACKER_WEB = "http://tracker.example/cgi-bin/roundup.cgi/bugs/"
39 # uncomment the following to have excessive debug output from test cases
40 # FIXME: tracker logging level should be increased by -v arguments
41 #   to 'run_tests.py' script
42 #config.LOGGING_FILENAME = "/tmp/logfile"
43 #config.LOGGING_LEVEL = "DEBUG"
44 config.init_logging()
46 def setupTracker(dirname, backend="anydbm"):
47     """Install and initialize new tracker in dirname; return tracker instance.
49     If the directory exists, it is wiped out before the operation.
51     """
52     global config
53     try:
54         shutil.rmtree(dirname)
55     except OSError, error:
56         if error.errno not in (errno.ENOENT, errno.ESRCH): raise
57     # create the instance
58     init.install(dirname, os.path.join(os.path.dirname(__file__), '..',
59         'templates/classic'))
60     init.write_select_db(dirname, backend)
61     config.save(os.path.join(dirname, 'config.ini'))
62     tracker = instance.open(dirname)
63     if tracker.exists():
64         tracker.nuke()
65         init.write_select_db(dirname, backend)
66     tracker.init(password.Password('sekrit'))
67     return tracker
69 def setupSchema(db, create, module):
70     status = module.Class(db, "status", name=String())
71     status.setkey("name")
72     priority = module.Class(db, "priority", name=String(), order=String())
73     priority.setkey("name")
74     user = module.Class(db, "user", username=String(), password=Password(),
75         assignable=Boolean(), age=Number(), roles=String(), address=String(),
76         supervisor=Link('user'),realname=String())
77     user.setkey("username")
78     file = module.FileClass(db, "file", name=String(), type=String(),
79         comment=String(indexme="yes"), fooz=Password())
80     file_nidx = module.FileClass(db, "file_nidx", content=String(indexme='no'))
81     issue = module.IssueClass(db, "issue", title=String(indexme="yes"),
82         status=Link("status"), nosy=Multilink("user"), deadline=Date(),
83         foo=Interval(), files=Multilink("file"), assignedto=Link('user'),
84         priority=Link('priority'), spam=Multilink('msg'),
85         feedback=Link('msg'))
86     stuff = module.Class(db, "stuff", stuff=String())
87     session = module.Class(db, 'session', title=String())
88     msg = module.FileClass(db, "msg", date=Date(),
89                            author=Link("user", do_journal='no'),
90                            files=Multilink('file'), inreplyto=String(),
91                            messageid=String(),
92                            recipients=Multilink("user", do_journal='no')
93                            )
94     session.disableJournalling()
95     db.post_init()
96     if create:
97         user.create(username="admin", roles='Admin',
98             password=password.Password('sekrit'))
99         user.create(username="fred", roles='User',
100             password=password.Password('sekrit'), address='fred@example.com')
101         status.create(name="unread")
102         status.create(name="in-progress")
103         status.create(name="testing")
104         status.create(name="resolved")
105         priority.create(name="feature", order="2")
106         priority.create(name="wish", order="3")
107         priority.create(name="bug", order="1")
108     db.commit()
110 class MyTestCase(unittest.TestCase):
111     def tearDown(self):
112         if hasattr(self, 'db'):
113             self.db.close()
114         if os.path.exists(config.DATABASE):
115             shutil.rmtree(config.DATABASE)
117 if os.environ.has_key('LOGGING_LEVEL'):
118     from roundup import rlog
119     config.logging = rlog.BasicLogging()
120     config.logging.setLevel(os.environ['LOGGING_LEVEL'])
121     config.logging.getLogger('hyperdb').setFormat('%(message)s')
123 class DBTest(MyTestCase):
124     def setUp(self):
125         # remove previous test, ignore errors
126         if os.path.exists(config.DATABASE):
127             shutil.rmtree(config.DATABASE)
128         os.makedirs(config.DATABASE + '/files')
129         self.db = self.module.Database(config, 'admin')
130         setupSchema(self.db, 1, self.module)
132     def testRefresh(self):
133         self.db.refresh_database()
135     #
136     # automatic properties (well, the two easy ones anyway)
137     #
138     def testCreatorProperty(self):
139         i = self.db.issue
140         id1 = i.create(title='spam')
141         self.db.commit()
142         self.db.close()
143         self.db = self.module.Database(config, 'fred')
144         setupSchema(self.db, 0, self.module)
145         i = self.db.issue
146         id2 = i.create(title='spam')
147         self.assertNotEqual(id1, id2)
148         self.assertNotEqual(i.get(id1, 'creator'), i.get(id2, 'creator'))
150     def testActorProperty(self):
151         i = self.db.issue
152         id1 = i.create(title='spam')
153         self.db.commit()
154         self.db.close()
155         self.db = self.module.Database(config, 'fred')
156         setupSchema(self.db, 0, self.module)
157         i = self.db.issue
158         i.set(id1, title='asfasd')
159         self.assertNotEqual(i.get(id1, 'creator'), i.get(id1, 'actor'))
161     # ID number controls
162     def testIDGeneration(self):
163         id1 = self.db.issue.create(title="spam", status='1')
164         id2 = self.db.issue.create(title="eggs", status='2')
165         self.assertNotEqual(id1, id2)
166     def testIDSetting(self):
167         # XXX numeric ids
168         self.db.setid('issue', 10)
169         id2 = self.db.issue.create(title="eggs", status='2')
170         self.assertEqual('11', id2)
172     #
173     # basic operations
174     #
175     def testEmptySet(self):
176         id1 = self.db.issue.create(title="spam", status='1')
177         self.db.issue.set(id1)
179     # String
180     def testStringChange(self):
181         for commit in (0,1):
182             # test set & retrieve
183             nid = self.db.issue.create(title="spam", status='1')
184             self.assertEqual(self.db.issue.get(nid, 'title'), 'spam')
186             # change and make sure we retrieve the correct value
187             self.db.issue.set(nid, title='eggs')
188             if commit: self.db.commit()
189             self.assertEqual(self.db.issue.get(nid, 'title'), 'eggs')
191     def testStringUnset(self):
192         for commit in (0,1):
193             nid = self.db.issue.create(title="spam", status='1')
194             if commit: self.db.commit()
195             self.assertEqual(self.db.issue.get(nid, 'title'), 'spam')
196             # make sure we can unset
197             self.db.issue.set(nid, title=None)
198             if commit: self.db.commit()
199             self.assertEqual(self.db.issue.get(nid, "title"), None)
201     # FileClass "content" property (no unset test)
202     def testFileClassContentChange(self):
203         for commit in (0,1):
204             # test set & retrieve
205             nid = self.db.file.create(content="spam")
206             self.assertEqual(self.db.file.get(nid, 'content'), 'spam')
208             # change and make sure we retrieve the correct value
209             self.db.file.set(nid, content='eggs')
210             if commit: self.db.commit()
211             self.assertEqual(self.db.file.get(nid, 'content'), 'eggs')
213     def testStringUnicode(self):
214         # test set & retrieve
215         ustr = u'\xe4\xf6\xfc\u20ac'.encode('utf8')
216         nid = self.db.issue.create(title=ustr, status='1')
217         self.assertEqual(self.db.issue.get(nid, 'title'), ustr)
219         # change and make sure we retrieve the correct value
220         ustr2 = u'change \u20ac change'.encode('utf8')
221         self.db.issue.set(nid, title=ustr2)
222         self.db.commit()
223         self.assertEqual(self.db.issue.get(nid, 'title'), ustr2)
225     # Link
226     def testLinkChange(self):
227         self.assertRaises(IndexError, self.db.issue.create, title="spam",
228             status='100')
229         for commit in (0,1):
230             nid = self.db.issue.create(title="spam", status='1')
231             if commit: self.db.commit()
232             self.assertEqual(self.db.issue.get(nid, "status"), '1')
233             self.db.issue.set(nid, status='2')
234             if commit: self.db.commit()
235             self.assertEqual(self.db.issue.get(nid, "status"), '2')
237     def testLinkUnset(self):
238         for commit in (0,1):
239             nid = self.db.issue.create(title="spam", status='1')
240             if commit: self.db.commit()
241             self.db.issue.set(nid, status=None)
242             if commit: self.db.commit()
243             self.assertEqual(self.db.issue.get(nid, "status"), None)
245     # Multilink
246     def testMultilinkChange(self):
247         for commit in (0,1):
248             self.assertRaises(IndexError, self.db.issue.create, title="spam",
249                 nosy=['foo%s'%commit])
250             u1 = self.db.user.create(username='foo%s'%commit)
251             u2 = self.db.user.create(username='bar%s'%commit)
252             nid = self.db.issue.create(title="spam", nosy=[u1])
253             if commit: self.db.commit()
254             self.assertEqual(self.db.issue.get(nid, "nosy"), [u1])
255             self.db.issue.set(nid, nosy=[])
256             if commit: self.db.commit()
257             self.assertEqual(self.db.issue.get(nid, "nosy"), [])
258             self.db.issue.set(nid, nosy=[u1,u2])
259             if commit: self.db.commit()
260             l = [u1,u2]; l.sort()
261             m = self.db.issue.get(nid, "nosy"); m.sort()
262             self.assertEqual(l, m)
264             # verify that when we pass None to an Multilink it sets
265             # it to an empty list
266             self.db.issue.set(nid, nosy=None)
267             if commit: self.db.commit()
268             self.assertEqual(self.db.issue.get(nid, "nosy"), [])
270     def testMultilinkChangeIterable(self):
271         for commit in (0,1):
272             # invalid nosy value assertion
273             self.assertRaises(IndexError, self.db.issue.create, title='spam',
274                 nosy=['foo%s'%commit])
275             # invalid type for nosy create
276             self.assertRaises(TypeError, self.db.issue.create, title='spam',
277                 nosy=1)
278             u1 = self.db.user.create(username='foo%s'%commit)
279             u2 = self.db.user.create(username='bar%s'%commit)
280             # try a couple of the built-in iterable types to make
281             # sure that we accept them and handle them properly
282             # try a set as input for the multilink
283             nid = self.db.issue.create(title="spam", nosy=sets.Set(u1))
284             if commit: self.db.commit()
285             self.assertEqual(self.db.issue.get(nid, "nosy"), [u1])
286             self.assertRaises(TypeError, self.db.issue.set, nid,
287                 nosy='invalid type')
288             # test with a tuple
289             self.db.issue.set(nid, nosy=tuple())
290             if commit: self.db.commit()
291             self.assertEqual(self.db.issue.get(nid, "nosy"), [])
292             # make sure we accept a frozen set
293             self.db.issue.set(nid, nosy=sets.Set([u1,u2]))
294             if commit: self.db.commit()
295             l = [u1,u2]; l.sort()
296             m = self.db.issue.get(nid, "nosy"); m.sort()
297             self.assertEqual(l, m)
300 # XXX one day, maybe...
301 #    def testMultilinkOrdering(self):
302 #        for i in range(10):
303 #            self.db.user.create(username='foo%s'%i)
304 #        i = self.db.issue.create(title="spam", nosy=['5','3','12','4'])
305 #        self.db.commit()
306 #        l = self.db.issue.get(i, "nosy")
307 #        # all backends should return the Multilink numeric-id-sorted
308 #        self.assertEqual(l, ['3', '4', '5', '12'])
310     # Date
311     def testDateChange(self):
312         self.assertRaises(TypeError, self.db.issue.create,
313             title='spam', deadline=1)
314         for commit in (0,1):
315             nid = self.db.issue.create(title="spam", status='1')
316             self.assertRaises(TypeError, self.db.issue.set, nid, deadline=1)
317             a = self.db.issue.get(nid, "deadline")
318             if commit: self.db.commit()
319             self.db.issue.set(nid, deadline=date.Date())
320             b = self.db.issue.get(nid, "deadline")
321             if commit: self.db.commit()
322             self.assertNotEqual(a, b)
323             self.assertNotEqual(b, date.Date('1970-1-1.00:00:00'))
324             # The 1970 date will fail for metakit -- it is used
325             # internally for storing NULL. The others would, too
326             # because metakit tries to convert date.timestamp to an int
327             # for storing and fails with an overflow.
328             for d in [date.Date (x) for x in '2038', '1970', '0033', '9999']:
329                 self.db.issue.set(nid, deadline=d)
330                 if commit: self.db.commit()
331                 c = self.db.issue.get(nid, "deadline")
332                 self.assertEqual(c, d)
334     def testDateLeapYear(self):
335         nid = self.db.issue.create(title='spam', status='1',
336             deadline=date.Date('2008-02-29'))
337         self.assertEquals(str(self.db.issue.get(nid, 'deadline')),
338             '2008-02-29.00:00:00')
339         self.assertEquals(self.db.issue.filter(None,
340             {'deadline': '2008-02-29'}), [nid])
341         self.db.issue.set(nid, deadline=date.Date('2008-03-01'))
342         self.assertEquals(str(self.db.issue.get(nid, 'deadline')),
343             '2008-03-01.00:00:00')
344         self.assertEquals(self.db.issue.filter(None,
345             {'deadline': '2008-02-29'}), [])
347     def testDateUnset(self):
348         for commit in (0,1):
349             nid = self.db.issue.create(title="spam", status='1')
350             self.db.issue.set(nid, deadline=date.Date())
351             if commit: self.db.commit()
352             self.assertNotEqual(self.db.issue.get(nid, "deadline"), None)
353             self.db.issue.set(nid, deadline=None)
354             if commit: self.db.commit()
355             self.assertEqual(self.db.issue.get(nid, "deadline"), None)
357     # Interval
358     def testIntervalChange(self):
359         self.assertRaises(TypeError, self.db.issue.create,
360             title='spam', foo=1)
361         for commit in (0,1):
362             nid = self.db.issue.create(title="spam", status='1')
363             self.assertRaises(TypeError, self.db.issue.set, nid, foo=1)
364             if commit: self.db.commit()
365             a = self.db.issue.get(nid, "foo")
366             i = date.Interval('-1d')
367             self.db.issue.set(nid, foo=i)
368             if commit: self.db.commit()
369             self.assertNotEqual(self.db.issue.get(nid, "foo"), a)
370             self.assertEqual(i, self.db.issue.get(nid, "foo"))
371             j = date.Interval('1y')
372             self.db.issue.set(nid, foo=j)
373             if commit: self.db.commit()
374             self.assertNotEqual(self.db.issue.get(nid, "foo"), i)
375             self.assertEqual(j, self.db.issue.get(nid, "foo"))
377     def testIntervalUnset(self):
378         for commit in (0,1):
379             nid = self.db.issue.create(title="spam", status='1')
380             self.db.issue.set(nid, foo=date.Interval('-1d'))
381             if commit: self.db.commit()
382             self.assertNotEqual(self.db.issue.get(nid, "foo"), None)
383             self.db.issue.set(nid, foo=None)
384             if commit: self.db.commit()
385             self.assertEqual(self.db.issue.get(nid, "foo"), None)
387     # Boolean
388     def testBooleanSet(self):
389         nid = self.db.user.create(username='one', assignable=1)
390         self.assertEqual(self.db.user.get(nid, "assignable"), 1)
391         nid = self.db.user.create(username='two', assignable=0)
392         self.assertEqual(self.db.user.get(nid, "assignable"), 0)
394     def testBooleanChange(self):
395         userid = self.db.user.create(username='foo', assignable=1)
396         self.assertEqual(1, self.db.user.get(userid, 'assignable'))
397         self.db.user.set(userid, assignable=0)
398         self.assertEqual(self.db.user.get(userid, 'assignable'), 0)
399         self.db.user.set(userid, assignable=1)
400         self.assertEqual(self.db.user.get(userid, 'assignable'), 1)
402     def testBooleanUnset(self):
403         nid = self.db.user.create(username='foo', assignable=1)
404         self.db.user.set(nid, assignable=None)
405         self.assertEqual(self.db.user.get(nid, "assignable"), None)
407     # Number
408     def testNumberChange(self):
409         nid = self.db.user.create(username='foo', age=1)
410         self.assertEqual(1, self.db.user.get(nid, 'age'))
411         self.db.user.set(nid, age=3)
412         self.assertNotEqual(self.db.user.get(nid, 'age'), 1)
413         self.db.user.set(nid, age=1.0)
414         self.assertEqual(self.db.user.get(nid, 'age'), 1)
415         self.db.user.set(nid, age=0)
416         self.assertEqual(self.db.user.get(nid, 'age'), 0)
418         nid = self.db.user.create(username='bar', age=0)
419         self.assertEqual(self.db.user.get(nid, 'age'), 0)
421     def testNumberUnset(self):
422         nid = self.db.user.create(username='foo', age=1)
423         self.db.user.set(nid, age=None)
424         self.assertEqual(self.db.user.get(nid, "age"), None)
426     # Password
427     def testPasswordChange(self):
428         x = password.Password('x')
429         userid = self.db.user.create(username='foo', password=x)
430         self.assertEqual(x, self.db.user.get(userid, 'password'))
431         self.assertEqual(self.db.user.get(userid, 'password'), 'x')
432         y = password.Password('y')
433         self.db.user.set(userid, password=y)
434         self.assertEqual(self.db.user.get(userid, 'password'), 'y')
435         self.assertRaises(TypeError, self.db.user.create, userid,
436             username='bar', password='x')
437         self.assertRaises(TypeError, self.db.user.set, userid, password='x')
439     def testPasswordUnset(self):
440         x = password.Password('x')
441         nid = self.db.user.create(username='foo', password=x)
442         self.db.user.set(nid, assignable=None)
443         self.assertEqual(self.db.user.get(nid, "assignable"), None)
445     # key value
446     def testKeyValue(self):
447         self.assertRaises(ValueError, self.db.user.create)
449         newid = self.db.user.create(username="spam")
450         self.assertEqual(self.db.user.lookup('spam'), newid)
451         self.db.commit()
452         self.assertEqual(self.db.user.lookup('spam'), newid)
453         self.db.user.retire(newid)
454         self.assertRaises(KeyError, self.db.user.lookup, 'spam')
456         # use the key again now that the old is retired
457         newid2 = self.db.user.create(username="spam")
458         self.assertNotEqual(newid, newid2)
459         # try to restore old node. this shouldn't succeed!
460         self.assertRaises(KeyError, self.db.user.restore, newid)
462         self.assertRaises(TypeError, self.db.issue.lookup, 'fubar')
464     # label property
465     def testLabelProp(self):
466         # key prop
467         self.assertEqual(self.db.status.labelprop(), 'name')
468         self.assertEqual(self.db.user.labelprop(), 'username')
469         # title
470         self.assertEqual(self.db.issue.labelprop(), 'title')
471         # name
472         self.assertEqual(self.db.file.labelprop(), 'name')
473         # id
474         self.assertEqual(self.db.stuff.labelprop(default_to_id=1), 'id')
476     # retirement
477     def testRetire(self):
478         self.db.issue.create(title="spam", status='1')
479         b = self.db.status.get('1', 'name')
480         a = self.db.status.list()
481         nodeids = self.db.status.getnodeids()
482         self.db.status.retire('1')
483         others = nodeids[:]
484         others.remove('1')
486         self.assertEqual(sets.Set(self.db.status.getnodeids()),
487             sets.Set(nodeids))
488         self.assertEqual(sets.Set(self.db.status.getnodeids(retired=True)),
489             sets.Set(['1']))
490         self.assertEqual(sets.Set(self.db.status.getnodeids(retired=False)),
491             sets.Set(others))
493         self.assert_(self.db.status.is_retired('1'))
495         # make sure the list is different
496         self.assertNotEqual(a, self.db.status.list())
498         # can still access the node if necessary
499         self.assertEqual(self.db.status.get('1', 'name'), b)
500         self.assertRaises(IndexError, self.db.status.set, '1', name='hello')
501         self.db.commit()
502         self.assert_(self.db.status.is_retired('1'))
503         self.assertEqual(self.db.status.get('1', 'name'), b)
504         self.assertNotEqual(a, self.db.status.list())
506         # try to restore retired node
507         self.db.status.restore('1')
509         self.assert_(not self.db.status.is_retired('1'))
511     def testCacheCreateSet(self):
512         self.db.issue.create(title="spam", status='1')
513         a = self.db.issue.get('1', 'title')
514         self.assertEqual(a, 'spam')
515         self.db.issue.set('1', title='ham')
516         b = self.db.issue.get('1', 'title')
517         self.assertEqual(b, 'ham')
519     def testSerialisation(self):
520         nid = self.db.issue.create(title="spam", status='1',
521             deadline=date.Date(), foo=date.Interval('-1d'))
522         self.db.commit()
523         assert isinstance(self.db.issue.get(nid, 'deadline'), date.Date)
524         assert isinstance(self.db.issue.get(nid, 'foo'), date.Interval)
525         uid = self.db.user.create(username="fozzy",
526             password=password.Password('t. bear'))
527         self.db.commit()
528         assert isinstance(self.db.user.get(uid, 'password'), password.Password)
530     def testTransactions(self):
531         # remember the number of items we started
532         num_issues = len(self.db.issue.list())
533         num_files = self.db.numfiles()
534         self.db.issue.create(title="don't commit me!", status='1')
535         self.assertNotEqual(num_issues, len(self.db.issue.list()))
536         self.db.rollback()
537         self.assertEqual(num_issues, len(self.db.issue.list()))
538         self.db.issue.create(title="please commit me!", status='1')
539         self.assertNotEqual(num_issues, len(self.db.issue.list()))
540         self.db.commit()
541         self.assertNotEqual(num_issues, len(self.db.issue.list()))
542         self.db.rollback()
543         self.assertNotEqual(num_issues, len(self.db.issue.list()))
544         self.db.file.create(name="test", type="text/plain", content="hi")
545         self.db.rollback()
546         self.assertEqual(num_files, self.db.numfiles())
547         for i in range(10):
548             self.db.file.create(name="test", type="text/plain",
549                     content="hi %d"%(i))
550             self.db.commit()
551         num_files2 = self.db.numfiles()
552         self.assertNotEqual(num_files, num_files2)
553         self.db.file.create(name="test", type="text/plain", content="hi")
554         self.db.rollback()
555         self.assertNotEqual(num_files, self.db.numfiles())
556         self.assertEqual(num_files2, self.db.numfiles())
558         # rollback / cache interaction
559         name1 = self.db.user.get('1', 'username')
560         self.db.user.set('1', username = name1+name1)
561         # get the prop so the info's forced into the cache (if there is one)
562         self.db.user.get('1', 'username')
563         self.db.rollback()
564         name2 = self.db.user.get('1', 'username')
565         self.assertEqual(name1, name2)
567     def testDestroyBlob(self):
568         # destroy an uncommitted blob
569         f1 = self.db.file.create(content='hello', type="text/plain")
570         self.db.commit()
571         fn = self.db.filename('file', f1)
572         self.db.file.destroy(f1)
573         self.db.commit()
574         self.assertEqual(os.path.exists(fn), False)
576     def testDestroyNoJournalling(self):
577         self.innerTestDestroy(klass=self.db.session)
579     def testDestroyJournalling(self):
580         self.innerTestDestroy(klass=self.db.issue)
582     def innerTestDestroy(self, klass):
583         newid = klass.create(title='Mr Friendly')
584         n = len(klass.list())
585         self.assertEqual(klass.get(newid, 'title'), 'Mr Friendly')
586         count = klass.count()
587         klass.destroy(newid)
588         self.assertNotEqual(count, klass.count())
589         self.assertRaises(IndexError, klass.get, newid, 'title')
590         self.assertNotEqual(len(klass.list()), n)
591         if klass.do_journal:
592             self.assertRaises(IndexError, klass.history, newid)
594         # now with a commit
595         newid = klass.create(title='Mr Friendly')
596         n = len(klass.list())
597         self.assertEqual(klass.get(newid, 'title'), 'Mr Friendly')
598         self.db.commit()
599         count = klass.count()
600         klass.destroy(newid)
601         self.assertNotEqual(count, klass.count())
602         self.assertRaises(IndexError, klass.get, newid, 'title')
603         self.db.commit()
604         self.assertRaises(IndexError, klass.get, newid, 'title')
605         self.assertNotEqual(len(klass.list()), n)
606         if klass.do_journal:
607             self.assertRaises(IndexError, klass.history, newid)
609         # now with a rollback
610         newid = klass.create(title='Mr Friendly')
611         n = len(klass.list())
612         self.assertEqual(klass.get(newid, 'title'), 'Mr Friendly')
613         self.db.commit()
614         count = klass.count()
615         klass.destroy(newid)
616         self.assertNotEqual(len(klass.list()), n)
617         self.assertRaises(IndexError, klass.get, newid, 'title')
618         self.db.rollback()
619         self.assertEqual(count, klass.count())
620         self.assertEqual(klass.get(newid, 'title'), 'Mr Friendly')
621         self.assertEqual(len(klass.list()), n)
622         if klass.do_journal:
623             self.assertNotEqual(klass.history(newid), [])
625     def testExceptions(self):
626         # this tests the exceptions that should be raised
627         ar = self.assertRaises
629         ar(KeyError, self.db.getclass, 'fubar')
631         #
632         # class create
633         #
634         # string property
635         ar(TypeError, self.db.status.create, name=1)
636         # id, creation, creator and activity properties are reserved
637         ar(KeyError, self.db.status.create, id=1)
638         ar(KeyError, self.db.status.create, creation=1)
639         ar(KeyError, self.db.status.create, creator=1)
640         ar(KeyError, self.db.status.create, activity=1)
641         ar(KeyError, self.db.status.create, actor=1)
642         # invalid property name
643         ar(KeyError, self.db.status.create, foo='foo')
644         # key name clash
645         ar(ValueError, self.db.status.create, name='unread')
646         # invalid link index
647         ar(IndexError, self.db.issue.create, title='foo', status='bar')
648         # invalid link value
649         ar(ValueError, self.db.issue.create, title='foo', status=1)
650         # invalid multilink type
651         ar(TypeError, self.db.issue.create, title='foo', status='1',
652             nosy='hello')
653         # invalid multilink index type
654         ar(ValueError, self.db.issue.create, title='foo', status='1',
655             nosy=[1])
656         # invalid multilink index
657         ar(IndexError, self.db.issue.create, title='foo', status='1',
658             nosy=['10'])
660         #
661         # key property
662         #
663         # key must be a String
664         ar(TypeError, self.db.file.setkey, 'fooz')
665         # key must exist
666         ar(KeyError, self.db.file.setkey, 'fubar')
668         #
669         # class get
670         #
671         # invalid node id
672         ar(IndexError, self.db.issue.get, '99', 'title')
673         # invalid property name
674         ar(KeyError, self.db.status.get, '2', 'foo')
676         #
677         # class set
678         #
679         # invalid node id
680         ar(IndexError, self.db.issue.set, '99', title='foo')
681         # invalid property name
682         ar(KeyError, self.db.status.set, '1', foo='foo')
683         # string property
684         ar(TypeError, self.db.status.set, '1', name=1)
685         # key name clash
686         ar(ValueError, self.db.status.set, '2', name='unread')
687         # set up a valid issue for me to work on
688         id = self.db.issue.create(title="spam", status='1')
689         # invalid link index
690         ar(IndexError, self.db.issue.set, id, title='foo', status='bar')
691         # invalid link value
692         ar(ValueError, self.db.issue.set, id, title='foo', status=1)
693         # invalid multilink type
694         ar(TypeError, self.db.issue.set, id, title='foo', status='1',
695             nosy='hello')
696         # invalid multilink index type
697         ar(ValueError, self.db.issue.set, id, title='foo', status='1',
698             nosy=[1])
699         # invalid multilink index
700         ar(IndexError, self.db.issue.set, id, title='foo', status='1',
701             nosy=['10'])
702         # NOTE: the following increment the username to avoid problems
703         # within metakit's backend (it creates the node, and then sets the
704         # info, so the create (and by a fluke the username set) go through
705         # before the age/assignable/etc. set, which raises the exception)
706         # invalid number value
707         ar(TypeError, self.db.user.create, username='foo', age='a')
708         # invalid boolean value
709         ar(TypeError, self.db.user.create, username='foo2', assignable='true')
710         nid = self.db.user.create(username='foo3')
711         # invalid number value
712         ar(TypeError, self.db.user.set, nid, age='a')
713         # invalid boolean value
714         ar(TypeError, self.db.user.set, nid, assignable='true')
716     def testAuditors(self):
717         class test:
718             called = False
719             def call(self, *args): self.called = True
720         create = test()
722         self.db.user.audit('create', create.call)
723         self.db.user.create(username="mary")
724         self.assertEqual(create.called, True)
726         set = test()
727         self.db.user.audit('set', set.call)
728         self.db.user.set('1', username="joe")
729         self.assertEqual(set.called, True)
731         retire = test()
732         self.db.user.audit('retire', retire.call)
733         self.db.user.retire('1')
734         self.assertEqual(retire.called, True)
736     def testAuditorTwo(self):
737         class test:
738             n = 0
739             def a(self, *args): self.call_a = self.n; self.n += 1
740             def b(self, *args): self.call_b = self.n; self.n += 1
741             def c(self, *args): self.call_c = self.n; self.n += 1
742         test = test()
743         self.db.user.audit('create', test.b, 1)
744         self.db.user.audit('create', test.a, 1)
745         self.db.user.audit('create', test.c, 2)
746         self.db.user.create(username="mary")
747         self.assertEqual(test.call_a, 0)
748         self.assertEqual(test.call_b, 1)
749         self.assertEqual(test.call_c, 2)
751     def testJournals(self):
752         muid = self.db.user.create(username="mary")
753         self.db.user.create(username="pete")
754         self.db.issue.create(title="spam", status='1')
755         self.db.commit()
757         # journal entry for issue create
758         journal = self.db.getjournal('issue', '1')
759         self.assertEqual(1, len(journal))
760         (nodeid, date_stamp, journaltag, action, params) = journal[0]
761         self.assertEqual(nodeid, '1')
762         self.assertEqual(journaltag, self.db.user.lookup('admin'))
763         self.assertEqual(action, 'create')
764         keys = params.keys()
765         keys.sort()
766         self.assertEqual(keys, [])
768         # journal entry for link
769         journal = self.db.getjournal('user', '1')
770         self.assertEqual(1, len(journal))
771         self.db.issue.set('1', assignedto='1')
772         self.db.commit()
773         journal = self.db.getjournal('user', '1')
774         self.assertEqual(2, len(journal))
775         (nodeid, date_stamp, journaltag, action, params) = journal[1]
776         self.assertEqual('1', nodeid)
777         self.assertEqual('1', journaltag)
778         self.assertEqual('link', action)
779         self.assertEqual(('issue', '1', 'assignedto'), params)
781         # wait a bit to keep proper order of journal entries
782         time.sleep(0.01)
783         # journal entry for unlink
784         self.db.setCurrentUser('mary')
785         self.db.issue.set('1', assignedto='2')
786         self.db.commit()
787         journal = self.db.getjournal('user', '1')
788         self.assertEqual(3, len(journal))
789         (nodeid, date_stamp, journaltag, action, params) = journal[2]
790         self.assertEqual('1', nodeid)
791         self.assertEqual(muid, journaltag)
792         self.assertEqual('unlink', action)
793         self.assertEqual(('issue', '1', 'assignedto'), params)
795         # test disabling journalling
796         # ... get the last entry
797         jlen = len(self.db.getjournal('user', '1'))
798         self.db.issue.disableJournalling()
799         self.db.issue.set('1', title='hello world')
800         self.db.commit()
801         # see if the change was journalled when it shouldn't have been
802         self.assertEqual(jlen,  len(self.db.getjournal('user', '1')))
803         jlen = len(self.db.getjournal('issue', '1'))
804         self.db.issue.enableJournalling()
805         self.db.issue.set('1', title='hello world 2')
806         self.db.commit()
807         # see if the change was journalled
808         self.assertNotEqual(jlen,  len(self.db.getjournal('issue', '1')))
810     def testJournalPreCommit(self):
811         id = self.db.user.create(username="mary")
812         self.assertEqual(len(self.db.getjournal('user', id)), 1)
813         self.db.commit()
815     def testPack(self):
816         id = self.db.issue.create(title="spam", status='1')
817         self.db.commit()
818         time.sleep(1)
819         self.db.issue.set(id, status='2')
820         self.db.commit()
822         # sleep for at least a second, then get a date to pack at
823         time.sleep(1)
824         pack_before = date.Date('.')
826         # wait another second and add one more entry
827         time.sleep(1)
828         self.db.issue.set(id, status='3')
829         self.db.commit()
830         jlen = len(self.db.getjournal('issue', id))
832         # pack
833         self.db.pack(pack_before)
835         # we should have the create and last set entries now
836         self.assertEqual(jlen-1, len(self.db.getjournal('issue', id)))
838     def testIndexerSearching(self):
839         f1 = self.db.file.create(content='hello', type="text/plain")
840         # content='world' has the wrong content-type and won't be indexed
841         f2 = self.db.file.create(content='world', type="text/frozz",
842             comment='blah blah')
843         i1 = self.db.issue.create(files=[f1, f2], title="flebble plop")
844         i2 = self.db.issue.create(title="flebble the frooz")
845         self.db.commit()
846         self.assertEquals(self.db.indexer.search([], self.db.issue), {})
847         self.assertEquals(self.db.indexer.search(['hello'], self.db.issue),
848             {i1: {'files': [f1]}})
849         self.assertEquals(self.db.indexer.search(['world'], self.db.issue), {})
850         self.assertEquals(self.db.indexer.search(['frooz'], self.db.issue),
851             {i2: {}})
852         self.assertEquals(self.db.indexer.search(['flebble'], self.db.issue),
853             {i1: {}, i2: {}})
855         # test AND'ing of search terms
856         self.assertEquals(self.db.indexer.search(['frooz', 'flebble'],
857             self.db.issue), {i2: {}})
859         # unindexed stopword
860         self.assertEquals(self.db.indexer.search(['the'], self.db.issue), {})
862     def testIndexerSearchingLink(self):
863         m1 = self.db.msg.create(content="one two")
864         i1 = self.db.issue.create(messages=[m1])
865         m2 = self.db.msg.create(content="two three")
866         i2 = self.db.issue.create(feedback=m2)
867         self.db.commit()
868         self.assertEquals(self.db.indexer.search(['two'], self.db.issue),
869             {i1: {'messages': [m1]}, i2: {'feedback': [m2]}})
871     def testIndexerSearchMulti(self):
872         m1 = self.db.msg.create(content="one two")
873         m2 = self.db.msg.create(content="two three")
874         i1 = self.db.issue.create(messages=[m1])
875         i2 = self.db.issue.create(spam=[m2])
876         self.db.commit()
877         self.assertEquals(self.db.indexer.search([], self.db.issue), {})
878         self.assertEquals(self.db.indexer.search(['one'], self.db.issue),
879             {i1: {'messages': [m1]}})
880         self.assertEquals(self.db.indexer.search(['two'], self.db.issue),
881             {i1: {'messages': [m1]}, i2: {'spam': [m2]}})
882         self.assertEquals(self.db.indexer.search(['three'], self.db.issue),
883             {i2: {'spam': [m2]}})
885     def testReindexingChange(self):
886         search = self.db.indexer.search
887         issue = self.db.issue
888         i1 = issue.create(title="flebble plop")
889         i2 = issue.create(title="flebble frooz")
890         self.db.commit()
891         self.assertEquals(search(['plop'], issue), {i1: {}})
892         self.assertEquals(search(['flebble'], issue), {i1: {}, i2: {}})
894         # change i1's title
895         issue.set(i1, title="plop")
896         self.db.commit()
897         self.assertEquals(search(['plop'], issue), {i1: {}})
898         self.assertEquals(search(['flebble'], issue), {i2: {}})
900     def testReindexingClear(self):
901         search = self.db.indexer.search
902         issue = self.db.issue
903         i1 = issue.create(title="flebble plop")
904         i2 = issue.create(title="flebble frooz")
905         self.db.commit()
906         self.assertEquals(search(['plop'], issue), {i1: {}})
907         self.assertEquals(search(['flebble'], issue), {i1: {}, i2: {}})
909         # unset i1's title
910         issue.set(i1, title="")
911         self.db.commit()
912         self.assertEquals(search(['plop'], issue), {})
913         self.assertEquals(search(['flebble'], issue), {i2: {}})
915     def testFileClassReindexing(self):
916         f1 = self.db.file.create(content='hello')
917         f2 = self.db.file.create(content='hello, world')
918         i1 = self.db.issue.create(files=[f1, f2])
919         self.db.commit()
920         d = self.db.indexer.search(['hello'], self.db.issue)
921         self.assert_(d.has_key(i1))
922         d[i1]['files'].sort()
923         self.assertEquals(d, {i1: {'files': [f1, f2]}})
924         self.assertEquals(self.db.indexer.search(['world'], self.db.issue),
925             {i1: {'files': [f2]}})
926         self.db.file.set(f1, content="world")
927         self.db.commit()
928         d = self.db.indexer.search(['world'], self.db.issue)
929         d[i1]['files'].sort()
930         self.assertEquals(d, {i1: {'files': [f1, f2]}})
931         self.assertEquals(self.db.indexer.search(['hello'], self.db.issue),
932             {i1: {'files': [f2]}})
934     def testFileClassIndexingNoNoNo(self):
935         f1 = self.db.file.create(content='hello')
936         self.db.commit()
937         self.assertEquals(self.db.indexer.search(['hello'], self.db.file),
938             {'1': {}})
940         f1 = self.db.file_nidx.create(content='hello')
941         self.db.commit()
942         self.assertEquals(self.db.indexer.search(['hello'], self.db.file_nidx),
943             {})
945     def testForcedReindexing(self):
946         self.db.issue.create(title="flebble frooz")
947         self.db.commit()
948         self.assertEquals(self.db.indexer.search(['flebble'], self.db.issue),
949             {'1': {}})
950         self.db.indexer.quiet = 1
951         self.db.indexer.force_reindex()
952         self.db.post_init()
953         self.db.indexer.quiet = 9
954         self.assertEquals(self.db.indexer.search(['flebble'], self.db.issue),
955             {'1': {}})
957     def testIndexingOnImport(self):
958         # import a message
959         msgcontent = 'Glrk'
960         msgid = self.db.msg.import_list(['content', 'files', 'recipients'],
961                                         [repr(msgcontent), '[]', '[]'])
962         msg_filename = self.db.filename(self.db.msg.classname, msgid,
963                                         create=1)
964         support.ensureParentsExist(msg_filename)
965         msg_file = open(msg_filename, 'w')
966         msg_file.write(msgcontent)
967         msg_file.close()
969         # import a file
970         filecontent = 'Brrk'
971         fileid = self.db.file.import_list(['content'], [repr(filecontent)])
972         file_filename = self.db.filename(self.db.file.classname, fileid,
973                                          create=1)
974         support.ensureParentsExist(file_filename)
975         file_file = open(file_filename, 'w')
976         file_file.write(filecontent)
977         file_file.close()
979         # import an issue
980         title = 'Bzzt'
981         nodeid = self.db.issue.import_list(['title', 'messages', 'files',
982             'spam', 'nosy', 'superseder'], [repr(title), repr([msgid]),
983             repr([fileid]), '[]', '[]', '[]'])
984         self.db.commit()
986         # Content of title attribute is indexed
987         self.assertEquals(self.db.indexer.search([title], self.db.issue),
988             {str(nodeid):{}})
989         # Content of message is indexed
990         self.assertEquals(self.db.indexer.search([msgcontent], self.db.issue),
991             {str(nodeid):{'messages':[str(msgid)]}})
992         # Content of file is indexed
993         self.assertEquals(self.db.indexer.search([filecontent], self.db.issue),
994             {str(nodeid):{'files':[str(fileid)]}})
998     #
999     # searching tests follow
1000     #
1001     def testFindIncorrectProperty(self):
1002         self.assertRaises(TypeError, self.db.issue.find, title='fubar')
1004     def _find_test_setup(self):
1005         self.db.file.create(content='')
1006         self.db.file.create(content='')
1007         self.db.user.create(username='')
1008         one = self.db.issue.create(status="1", nosy=['1'])
1009         two = self.db.issue.create(status="2", nosy=['2'], files=['1'],
1010             assignedto='2')
1011         three = self.db.issue.create(status="1", nosy=['1','2'])
1012         four = self.db.issue.create(status="3", assignedto='1',
1013             files=['1','2'])
1014         return one, two, three, four
1016     def testFindLink(self):
1017         one, two, three, four = self._find_test_setup()
1018         got = self.db.issue.find(status='1')
1019         got.sort()
1020         self.assertEqual(got, [one, three])
1021         got = self.db.issue.find(status={'1':1})
1022         got.sort()
1023         self.assertEqual(got, [one, three])
1025     def testFindLinkFail(self):
1026         self._find_test_setup()
1027         self.assertEqual(self.db.issue.find(status='4'), [])
1028         self.assertEqual(self.db.issue.find(status={'4':1}), [])
1030     def testFindLinkUnset(self):
1031         one, two, three, four = self._find_test_setup()
1032         got = self.db.issue.find(assignedto=None)
1033         got.sort()
1034         self.assertEqual(got, [one, three])
1035         got = self.db.issue.find(assignedto={None:1})
1036         got.sort()
1037         self.assertEqual(got, [one, three])
1039     def testFindMultipleLink(self):
1040         one, two, three, four = self._find_test_setup()
1041         l = self.db.issue.find(status={'1':1, '3':1})
1042         l.sort()
1043         self.assertEqual(l, [one, three, four])
1044         l = self.db.issue.find(assignedto={None:1, '1':1})
1045         l.sort()
1046         self.assertEqual(l, [one, three, four])
1048     def testFindMultilink(self):
1049         one, two, three, four = self._find_test_setup()
1050         got = self.db.issue.find(nosy='2')
1051         got.sort()
1052         self.assertEqual(got, [two, three])
1053         got = self.db.issue.find(nosy={'2':1})
1054         got.sort()
1055         self.assertEqual(got, [two, three])
1056         got = self.db.issue.find(nosy={'2':1}, files={})
1057         got.sort()
1058         self.assertEqual(got, [two, three])
1060     def testFindMultiMultilink(self):
1061         one, two, three, four = self._find_test_setup()
1062         got = self.db.issue.find(nosy='2', files='1')
1063         got.sort()
1064         self.assertEqual(got, [two, three, four])
1065         got = self.db.issue.find(nosy={'2':1}, files={'1':1})
1066         got.sort()
1067         self.assertEqual(got, [two, three, four])
1069     def testFindMultilinkFail(self):
1070         self._find_test_setup()
1071         self.assertEqual(self.db.issue.find(nosy='3'), [])
1072         self.assertEqual(self.db.issue.find(nosy={'3':1}), [])
1074     def testFindMultilinkUnset(self):
1075         self._find_test_setup()
1076         self.assertEqual(self.db.issue.find(nosy={}), [])
1078     def testFindLinkAndMultilink(self):
1079         one, two, three, four = self._find_test_setup()
1080         got = self.db.issue.find(status='1', nosy='2')
1081         got.sort()
1082         self.assertEqual(got, [one, two, three])
1083         got = self.db.issue.find(status={'1':1}, nosy={'2':1})
1084         got.sort()
1085         self.assertEqual(got, [one, two, three])
1087     def testFindRetired(self):
1088         one, two, three, four = self._find_test_setup()
1089         self.assertEqual(len(self.db.issue.find(status='1')), 2)
1090         self.db.issue.retire(one)
1091         self.assertEqual(len(self.db.issue.find(status='1')), 1)
1093     def testStringFind(self):
1094         self.assertRaises(TypeError, self.db.issue.stringFind, status='1')
1096         ids = []
1097         ids.append(self.db.issue.create(title="spam"))
1098         self.db.issue.create(title="not spam")
1099         ids.append(self.db.issue.create(title="spam"))
1100         ids.sort()
1101         got = self.db.issue.stringFind(title='spam')
1102         got.sort()
1103         self.assertEqual(got, ids)
1104         self.assertEqual(self.db.issue.stringFind(title='fubar'), [])
1106         # test retiring a node
1107         self.db.issue.retire(ids[0])
1108         self.assertEqual(len(self.db.issue.stringFind(title='spam')), 1)
1110     def filteringSetup(self):
1111         for user in (
1112                 {'username': 'bleep', 'age': 1},
1113                 {'username': 'blop', 'age': 1.5},
1114                 {'username': 'blorp', 'age': 2}):
1115             self.db.user.create(**user)
1116         iss = self.db.issue
1117         file_content = ''.join([chr(i) for i in range(255)])
1118         f = self.db.file.create(content=file_content)
1119         for issue in (
1120                 {'title': 'issue one', 'status': '2', 'assignedto': '1',
1121                     'foo': date.Interval('1:10'), 'priority': '3',
1122                     'deadline': date.Date('2003-02-16.22:50')},
1123                 {'title': 'issue two', 'status': '1', 'assignedto': '2',
1124                     'foo': date.Interval('1d'), 'priority': '3',
1125                     'deadline': date.Date('2003-01-01.00:00')},
1126                 {'title': 'issue three', 'status': '1', 'priority': '2',
1127                     'nosy': ['1','2'], 'deadline': date.Date('2003-02-18')},
1128                 {'title': 'non four', 'status': '3',
1129                     'foo': date.Interval('0:10'), 'priority': '2',
1130                     'nosy': ['1','2','3'], 'deadline': date.Date('2004-03-08'),
1131                     'files': [f]}):
1132             self.db.issue.create(**issue)
1133         self.db.commit()
1134         return self.assertEqual, self.db.issue.filter
1136     def testFilteringID(self):
1137         ae, filt = self.filteringSetup()
1138         ae(filt(None, {'id': '1'}, ('+','id'), (None,None)), ['1'])
1139         ae(filt(None, {'id': '2'}, ('+','id'), (None,None)), ['2'])
1140         ae(filt(None, {'id': '100'}, ('+','id'), (None,None)), [])
1142     def testFilteringNumber(self):
1143         self.filteringSetup()
1144         ae, filt = self.assertEqual, self.db.user.filter
1145         ae(filt(None, {'age': '1'}, ('+','id'), (None,None)), ['3'])
1146         ae(filt(None, {'age': '1.5'}, ('+','id'), (None,None)), ['4'])
1147         ae(filt(None, {'age': '2'}, ('+','id'), (None,None)), ['5'])
1148         ae(filt(None, {'age': ['1','2']}, ('+','id'), (None,None)), ['3','5'])
1150     def testFilteringString(self):
1151         ae, filt = self.filteringSetup()
1152         ae(filt(None, {'title': ['one']}, ('+','id'), (None,None)), ['1'])
1153         ae(filt(None, {'title': ['issue one']}, ('+','id'), (None,None)),
1154             ['1'])
1155         ae(filt(None, {'title': ['issue', 'one']}, ('+','id'), (None,None)),
1156             ['1'])
1157         ae(filt(None, {'title': ['issue']}, ('+','id'), (None,None)),
1158             ['1','2','3'])
1159         ae(filt(None, {'title': ['one', 'two']}, ('+','id'), (None,None)),
1160             [])
1162     def testFilteringLink(self):
1163         ae, filt = self.filteringSetup()
1164         ae(filt(None, {'status': '1'}, ('+','id'), (None,None)), ['2','3'])
1165         ae(filt(None, {'assignedto': '-1'}, ('+','id'), (None,None)), ['3','4'])
1166         ae(filt(None, {'assignedto': None}, ('+','id'), (None,None)), ['3','4'])
1167         ae(filt(None, {'assignedto': [None]}, ('+','id'), (None,None)),
1168             ['3','4'])
1169         ae(filt(None, {'assignedto': ['-1', None]}, ('+','id'), (None,None)),
1170             ['3','4'])
1171         ae(filt(None, {'assignedto': ['1', None]}, ('+','id'), (None,None)),
1172             ['1', '3','4'])
1174     def testFilteringMultilinkAndGroup(self):
1175         """testFilteringMultilinkAndGroup:
1176         See roundup Bug 1541128: apparently grouping by something and
1177         searching a Multilink failed with MySQL 5.0
1178         """
1179         ae, filt = self.filteringSetup()
1180         ae(filt(None, {'files': '1'}, ('-','activity'), ('+','status')), ['4'])
1182     def testFilteringRetired(self):
1183         ae, filt = self.filteringSetup()
1184         self.db.issue.retire('2')
1185         ae(filt(None, {'status': '1'}, ('+','id'), (None,None)), ['3'])
1187     def testFilteringMultilink(self):
1188         ae, filt = self.filteringSetup()
1189         ae(filt(None, {'nosy': '3'}, ('+','id'), (None,None)), ['4'])
1190         ae(filt(None, {'nosy': '-1'}, ('+','id'), (None,None)), ['1', '2'])
1191         ae(filt(None, {'nosy': ['1','2']}, ('+', 'status'),
1192             ('-', 'deadline')), ['4', '3'])
1194     def testFilteringMany(self):
1195         ae, filt = self.filteringSetup()
1196         ae(filt(None, {'nosy': '2', 'status': '1'}, ('+','id'), (None,None)),
1197             ['3'])
1199     def testFilteringRangeBasic(self):
1200         ae, filt = self.filteringSetup()
1201         ae(filt(None, {'deadline': 'from 2003-02-10 to 2003-02-23'}), ['1','3'])
1202         ae(filt(None, {'deadline': '2003-02-10; 2003-02-23'}), ['1','3'])
1203         ae(filt(None, {'deadline': '; 2003-02-16'}), ['2'])
1205     def testFilteringRangeTwoSyntaxes(self):
1206         ae, filt = self.filteringSetup()
1207         ae(filt(None, {'deadline': 'from 2003-02-16'}), ['1', '3', '4'])
1208         ae(filt(None, {'deadline': '2003-02-16;'}), ['1', '3', '4'])
1210     def testFilteringRangeYearMonthDay(self):
1211         ae, filt = self.filteringSetup()
1212         ae(filt(None, {'deadline': '2002'}), [])
1213         ae(filt(None, {'deadline': '2003'}), ['1', '2', '3'])
1214         ae(filt(None, {'deadline': '2004'}), ['4'])
1215         ae(filt(None, {'deadline': '2003-02-16'}), ['1'])
1216         ae(filt(None, {'deadline': '2003-02-17'}), [])
1218     def testFilteringRangeMonths(self):
1219         ae, filt = self.filteringSetup()
1220         for month in range(1, 13):
1221             for n in range(1, month+1):
1222                 i = self.db.issue.create(title='%d.%d'%(month, n),
1223                     deadline=date.Date('2001-%02d-%02d.00:00'%(month, n)))
1224         self.db.commit()
1226         for month in range(1, 13):
1227             r = filt(None, dict(deadline='2001-%02d'%month))
1228             assert len(r) == month, 'month %d != length %d'%(month, len(r))
1230     def testFilteringRangeInterval(self):
1231         ae, filt = self.filteringSetup()
1232         ae(filt(None, {'foo': 'from 0:50 to 2:00'}), ['1'])
1233         ae(filt(None, {'foo': 'from 0:50 to 1d 2:00'}), ['1', '2'])
1234         ae(filt(None, {'foo': 'from 5:50'}), ['2'])
1235         ae(filt(None, {'foo': 'to 0:05'}), [])
1237     def testFilteringRangeGeekInterval(self):
1238         ae, filt = self.filteringSetup()
1239         for issue in (
1240                 { 'deadline': date.Date('. -2d')},
1241                 { 'deadline': date.Date('. -1d')},
1242                 { 'deadline': date.Date('. -8d')},
1243                 ):
1244             self.db.issue.create(**issue)
1245         ae(filt(None, {'deadline': '-2d;'}), ['5', '6'])
1246         ae(filt(None, {'deadline': '-1d;'}), ['6'])
1247         ae(filt(None, {'deadline': '-1w;'}), ['5', '6'])
1249     def testFilteringIntervalSort(self):
1250         # 1: '1:10'
1251         # 2: '1d'
1252         # 3: None
1253         # 4: '0:10'
1254         ae, filt = self.filteringSetup()
1255         # ascending should sort None, 1:10, 1d
1256         ae(filt(None, {}, ('+','foo'), (None,None)), ['3', '4', '1', '2'])
1257         # descending should sort 1d, 1:10, None
1258         ae(filt(None, {}, ('-','foo'), (None,None)), ['2', '1', '4', '3'])
1260     def testFilteringStringSort(self):
1261         # 1: 'issue one'
1262         # 2: 'issue two'
1263         # 3: 'issue three'
1264         # 4: 'non four'
1265         ae, filt = self.filteringSetup()
1266         ae(filt(None, {}, ('+','title')), ['1', '3', '2', '4'])
1267         ae(filt(None, {}, ('-','title')), ['4', '2', '3', '1'])
1268         # Test string case: For now allow both, w/wo case matching.
1269         # 1: 'issue one'
1270         # 2: 'issue two'
1271         # 3: 'Issue three'
1272         # 4: 'non four'
1273         self.db.issue.set('3', title='Issue three')
1274         ae(filt(None, {}, ('+','title')), ['1', '3', '2', '4'])
1275         ae(filt(None, {}, ('-','title')), ['4', '2', '3', '1'])
1276         # Obscure bug in anydbm backend trying to convert to number
1277         # 1: '1st issue'
1278         # 2: '2'
1279         # 3: 'Issue three'
1280         # 4: 'non four'
1281         self.db.issue.set('1', title='1st issue')
1282         self.db.issue.set('2', title='2')
1283         ae(filt(None, {}, ('+','title')), ['1', '2', '3', '4'])
1284         ae(filt(None, {}, ('-','title')), ['4', '3', '2', '1'])
1286     def testFilteringMultilinkSort(self):
1287         # 1: []                 Reverse:  1: []
1288         # 2: []                           2: []
1289         # 3: ['admin','fred']             3: ['fred','admin']
1290         # 4: ['admin','bleep','fred']     4: ['fred','bleep','admin']
1291         # Note the sort order for the multilink doen't change when
1292         # reversing the sort direction due to the re-sorting of the
1293         # multilink!
1294         ae, filt = self.filteringSetup()
1295         ae(filt(None, {}, ('+','nosy'), (None,None)), ['1', '2', '4', '3'])
1296         ae(filt(None, {}, ('-','nosy'), (None,None)), ['4', '3', '1', '2'])
1298     def testFilteringMultilinkSortGroup(self):
1299         # 1: status: 2 "in-progress" nosy: []
1300         # 2: status: 1 "unread"      nosy: []
1301         # 3: status: 1 "unread"      nosy: ['admin','fred']
1302         # 4: status: 3 "testing"     nosy: ['admin','bleep','fred']
1303         ae, filt = self.filteringSetup()
1304         ae(filt(None, {}, ('+','nosy'), ('+','status')), ['1', '4', '2', '3'])
1305         ae(filt(None, {}, ('-','nosy'), ('+','status')), ['1', '4', '3', '2'])
1306         ae(filt(None, {}, ('+','nosy'), ('-','status')), ['2', '3', '4', '1'])
1307         ae(filt(None, {}, ('-','nosy'), ('-','status')), ['3', '2', '4', '1'])
1308         ae(filt(None, {}, ('+','status'), ('+','nosy')), ['1', '2', '4', '3'])
1309         ae(filt(None, {}, ('-','status'), ('+','nosy')), ['2', '1', '4', '3'])
1310         ae(filt(None, {}, ('+','status'), ('-','nosy')), ['4', '3', '1', '2'])
1311         ae(filt(None, {}, ('-','status'), ('-','nosy')), ['4', '3', '2', '1'])
1313     def testFilteringLinkSortGroup(self):
1314         # 1: status: 2 -> 'i', priority: 3 -> 1
1315         # 2: status: 1 -> 'u', priority: 3 -> 1
1316         # 3: status: 1 -> 'u', priority: 2 -> 3
1317         # 4: status: 3 -> 't', priority: 2 -> 3
1318         ae, filt = self.filteringSetup()
1319         ae(filt(None, {}, ('+','status'), ('+','priority')),
1320             ['1', '2', '4', '3'])
1321         ae(filt(None, {'priority':'2'}, ('+','status'), ('+','priority')),
1322             ['4', '3'])
1323         ae(filt(None, {'priority.order':'3'}, ('+','status'), ('+','priority')),
1324             ['4', '3'])
1325         ae(filt(None, {'priority':['2','3']}, ('+','priority'), ('+','status')),
1326             ['1', '4', '2', '3'])
1327         ae(filt(None, {}, ('+','priority'), ('+','status')),
1328             ['1', '4', '2', '3'])
1330     def testFilteringDateSort(self):
1331         # '1': '2003-02-16.22:50'
1332         # '2': '2003-01-01.00:00'
1333         # '3': '2003-02-18'
1334         # '4': '2004-03-08'
1335         ae, filt = self.filteringSetup()
1336         # ascending
1337         ae(filt(None, {}, ('+','deadline'), (None,None)), ['2', '1', '3', '4'])
1338         # descending
1339         ae(filt(None, {}, ('-','deadline'), (None,None)), ['4', '3', '1', '2'])
1341     def testFilteringDateSortPriorityGroup(self):
1342         # '1': '2003-02-16.22:50'  1 => 2
1343         # '2': '2003-01-01.00:00'  3 => 1
1344         # '3': '2003-02-18'        2 => 3
1345         # '4': '2004-03-08'        1 => 2
1346         ae, filt = self.filteringSetup()
1348         # ascending
1349         ae(filt(None, {}, ('+','deadline'), ('+','priority')),
1350             ['2', '1', '3', '4'])
1351         ae(filt(None, {}, ('-','deadline'), ('+','priority')),
1352             ['1', '2', '4', '3'])
1353         # descending
1354         ae(filt(None, {}, ('+','deadline'), ('-','priority')),
1355             ['3', '4', '2', '1'])
1356         ae(filt(None, {}, ('-','deadline'), ('-','priority')),
1357             ['4', '3', '1', '2'])
1359     def filteringSetupTransitiveSearch(self):
1360         u_m = {}
1361         k = 30
1362         for user in (
1363                 {'username': 'ceo', 'age': 129},
1364                 {'username': 'grouplead1', 'age': 29, 'supervisor': '3'},
1365                 {'username': 'grouplead2', 'age': 29, 'supervisor': '3'},
1366                 {'username': 'worker1', 'age': 25, 'supervisor' : '4'},
1367                 {'username': 'worker2', 'age': 24, 'supervisor' : '4'},
1368                 {'username': 'worker3', 'age': 23, 'supervisor' : '5'},
1369                 {'username': 'worker4', 'age': 22, 'supervisor' : '5'},
1370                 {'username': 'worker5', 'age': 21, 'supervisor' : '5'}):
1371             u = self.db.user.create(**user)
1372             u_m [u] = self.db.msg.create(author = u, content = ' '
1373                 , date = date.Date ('2006-01-%s' % k))
1374             k -= 1
1375         iss = self.db.issue
1376         for issue in (
1377                 {'title': 'ts1', 'status': '2', 'assignedto': '6',
1378                     'priority': '3', 'messages' : [u_m ['6']], 'nosy' : ['4']},
1379                 {'title': 'ts2', 'status': '1', 'assignedto': '6',
1380                     'priority': '3', 'messages' : [u_m ['6']], 'nosy' : ['5']},
1381                 {'title': 'ts4', 'status': '2', 'assignedto': '7',
1382                     'priority': '3', 'messages' : [u_m ['7']]},
1383                 {'title': 'ts5', 'status': '1', 'assignedto': '8',
1384                     'priority': '3', 'messages' : [u_m ['8']]},
1385                 {'title': 'ts6', 'status': '2', 'assignedto': '9',
1386                     'priority': '3', 'messages' : [u_m ['9']]},
1387                 {'title': 'ts7', 'status': '1', 'assignedto': '10',
1388                     'priority': '3', 'messages' : [u_m ['10']]},
1389                 {'title': 'ts8', 'status': '2', 'assignedto': '10',
1390                     'priority': '3', 'messages' : [u_m ['10']]},
1391                 {'title': 'ts9', 'status': '1', 'assignedto': '10',
1392                     'priority': '3', 'messages' : [u_m ['10'], u_m ['9']]}):
1393             self.db.issue.create(**issue)
1394         return self.assertEqual, self.db.issue.filter
1396     def testFilteringTransitiveLinkUser(self):
1397         ae, filt = self.filteringSetupTransitiveSearch()
1398         ufilt = self.db.user.filter
1399         ae(ufilt(None, {'supervisor.username': 'ceo'}, ('+','username')),
1400             ['4', '5'])
1401         ae(ufilt(None, {'supervisor.supervisor.username': 'ceo'},
1402             ('+','username')), ['6', '7', '8', '9', '10'])
1403         ae(ufilt(None, {'supervisor.supervisor': '3'}, ('+','username')),
1404             ['6', '7', '8', '9', '10'])
1405         ae(ufilt(None, {'supervisor.supervisor.id': '3'}, ('+','username')),
1406             ['6', '7', '8', '9', '10'])
1407         ae(ufilt(None, {'supervisor.username': 'grouplead1'}, ('+','username')),
1408             ['6', '7'])
1409         ae(ufilt(None, {'supervisor.username': 'grouplead2'}, ('+','username')),
1410             ['8', '9', '10'])
1411         ae(ufilt(None, {'supervisor.username': 'grouplead2',
1412             'supervisor.supervisor.username': 'ceo'}, ('+','username')),
1413             ['8', '9', '10'])
1414         ae(ufilt(None, {'supervisor.supervisor': '3', 'supervisor': '4'},
1415             ('+','username')), ['6', '7'])
1417     def testFilteringTransitiveLinkSort(self):
1418         ae, filt = self.filteringSetupTransitiveSearch()
1419         ufilt = self.db.user.filter
1420         # Need to make ceo his own (and first two users') supervisor,
1421         # otherwise we will depend on sorting order of NULL values.
1422         # Leave that to a separate test.
1423         self.db.user.set('1', supervisor = '3')
1424         self.db.user.set('2', supervisor = '3')
1425         self.db.user.set('3', supervisor = '3')
1426         ae(ufilt(None, {'supervisor':'3'}, []), ['1', '2', '3', '4', '5'])
1427         ae(ufilt(None, {}, [('+','supervisor.supervisor.supervisor'),
1428             ('+','supervisor.supervisor'), ('+','supervisor'),
1429             ('+','username')]),
1430             ['1', '3', '2', '4', '5', '6', '7', '8', '9', '10'])
1431         ae(ufilt(None, {}, [('+','supervisor.supervisor.supervisor'),
1432             ('-','supervisor.supervisor'), ('-','supervisor'),
1433             ('+','username')]),
1434             ['8', '9', '10', '6', '7', '1', '3', '2', '4', '5'])
1435         ae(filt(None, {}, [('+','assignedto.supervisor.supervisor.supervisor'),
1436             ('+','assignedto.supervisor.supervisor'),
1437             ('+','assignedto.supervisor'), ('+','assignedto')]),
1438             ['1', '2', '3', '4', '5', '6', '7', '8'])
1439         ae(filt(None, {}, [('+','assignedto.supervisor.supervisor.supervisor'),
1440             ('+','assignedto.supervisor.supervisor'),
1441             ('-','assignedto.supervisor'), ('+','assignedto')]),
1442             ['4', '5', '6', '7', '8', '1', '2', '3'])
1443         ae(filt(None, {}, [('+','assignedto.supervisor.supervisor.supervisor'),
1444             ('+','assignedto.supervisor.supervisor'),
1445             ('+','assignedto.supervisor'), ('+','assignedto'),
1446             ('-','status')]),
1447             ['2', '1', '3', '4', '5', '6', '8', '7'])
1448         ae(filt(None, {}, [('+','assignedto.supervisor.supervisor.supervisor'),
1449             ('+','assignedto.supervisor.supervisor'),
1450             ('+','assignedto.supervisor'), ('+','assignedto'),
1451             ('+','status')]),
1452             ['1', '2', '3', '4', '5', '7', '6', '8'])
1453         ae(filt(None, {}, [('+','assignedto.supervisor.supervisor.supervisor'),
1454             ('+','assignedto.supervisor.supervisor'),
1455             ('-','assignedto.supervisor'), ('+','assignedto'), ('+','status')]),
1456             ['4', '5', '7', '6', '8', '1', '2', '3'])
1457         ae(filt(None, {'assignedto':['6','7','8','9','10']},
1458             [('+','assignedto.supervisor.supervisor.supervisor'),
1459             ('+','assignedto.supervisor.supervisor'),
1460             ('-','assignedto.supervisor'), ('+','assignedto'), ('+','status')]),
1461             ['4', '5', '7', '6', '8', '1', '2', '3'])
1462         ae(filt(None, {'assignedto':['6','7','8','9']},
1463             [('+','assignedto.supervisor.supervisor.supervisor'),
1464             ('+','assignedto.supervisor.supervisor'),
1465             ('-','assignedto.supervisor'), ('+','assignedto'), ('+','status')]),
1466             ['4', '5', '1', '2', '3'])
1468     def testFilteringTransitiveLinkSortNull(self):
1469         """Check sorting of NULL values"""
1470         ae, filt = self.filteringSetupTransitiveSearch()
1471         ufilt = self.db.user.filter
1472         ae(ufilt(None, {}, [('+','supervisor.supervisor.supervisor'),
1473             ('+','supervisor.supervisor'), ('+','supervisor'),
1474             ('+','username')]),
1475             ['1', '3', '2', '4', '5', '6', '7', '8', '9', '10'])
1476         ae(ufilt(None, {}, [('+','supervisor.supervisor.supervisor'),
1477             ('-','supervisor.supervisor'), ('-','supervisor'),
1478             ('+','username')]),
1479             ['8', '9', '10', '6', '7', '4', '5', '1', '3', '2'])
1480         ae(filt(None, {}, [('+','assignedto.supervisor.supervisor.supervisor'),
1481             ('+','assignedto.supervisor.supervisor'),
1482             ('+','assignedto.supervisor'), ('+','assignedto')]),
1483             ['1', '2', '3', '4', '5', '6', '7', '8'])
1484         ae(filt(None, {}, [('+','assignedto.supervisor.supervisor.supervisor'),
1485             ('+','assignedto.supervisor.supervisor'),
1486             ('-','assignedto.supervisor'), ('+','assignedto')]),
1487             ['4', '5', '6', '7', '8', '1', '2', '3'])
1489     def testFilteringTransitiveLinkIssue(self):
1490         ae, filt = self.filteringSetupTransitiveSearch()
1491         ae(filt(None, {'assignedto.supervisor.username': 'grouplead1'},
1492             ('+','id')), ['1', '2', '3'])
1493         ae(filt(None, {'assignedto.supervisor.username': 'grouplead2'},
1494             ('+','id')), ['4', '5', '6', '7', '8'])
1495         ae(filt(None, {'assignedto.supervisor.username': 'grouplead2',
1496                        'status': '1'}, ('+','id')), ['4', '6', '8'])
1497         ae(filt(None, {'assignedto.supervisor.username': 'grouplead2',
1498                        'status': '2'}, ('+','id')), ['5', '7'])
1499         ae(filt(None, {'assignedto.supervisor.username': ['grouplead2'],
1500                        'status': '2'}, ('+','id')), ['5', '7'])
1501         ae(filt(None, {'assignedto.supervisor': ['4', '5'], 'status': '2'},
1502             ('+','id')), ['1', '3', '5', '7'])
1504     def testFilteringTransitiveMultilink(self):
1505         ae, filt = self.filteringSetupTransitiveSearch()
1506         ae(filt(None, {'messages.author.username': 'grouplead1'},
1507             ('+','id')), [])
1508         ae(filt(None, {'messages.author': '6'},
1509             ('+','id')), ['1', '2'])
1510         ae(filt(None, {'messages.author.id': '6'},
1511             ('+','id')), ['1', '2'])
1512         ae(filt(None, {'messages.author.username': 'worker1'},
1513             ('+','id')), ['1', '2'])
1514         ae(filt(None, {'messages.author': '10'},
1515             ('+','id')), ['6', '7', '8'])
1516         ae(filt(None, {'messages.author': '9'},
1517             ('+','id')), ['5', '8'])
1518         ae(filt(None, {'messages.author': ['9', '10']},
1519             ('+','id')), ['5', '6', '7', '8'])
1520         ae(filt(None, {'messages.author': ['8', '9']},
1521             ('+','id')), ['4', '5', '8'])
1522         ae(filt(None, {'messages.author': ['8', '9'], 'status' : '1'},
1523             ('+','id')), ['4', '8'])
1524         ae(filt(None, {'messages.author': ['8', '9'], 'status' : '2'},
1525             ('+','id')), ['5'])
1526         ae(filt(None, {'messages.author': ['8', '9', '10'],
1527             'messages.date': '2006-01-22.21:00;2006-01-23'}, ('+','id')),
1528             ['6', '7', '8'])
1529         ae(filt(None, {'nosy.supervisor.username': 'ceo'},
1530             ('+','id')), ['1', '2'])
1531         ae(filt(None, {'messages.author': ['6', '9']},
1532             ('+','id')), ['1', '2', '5', '8'])
1533         ae(filt(None, {'messages': ['5', '7']},
1534             ('+','id')), ['3', '5', '8'])
1535         ae(filt(None, {'messages.author': ['6', '9'], 'messages': ['5', '7']},
1536             ('+','id')), ['5', '8'])
1538     def testFilteringTransitiveMultilinkSort(self):
1539         ae, filt = self.filteringSetupTransitiveSearch()
1540         ae(filt(None, {}, [('+','messages.author')]),
1541             ['1', '2', '3', '4', '5', '8', '6', '7'])
1542         ae(filt(None, {}, [('-','messages.author')]),
1543             ['8', '6', '7', '5', '4', '3', '1', '2'])
1544         ae(filt(None, {}, [('+','messages.date')]),
1545             ['6', '7', '8', '5', '4', '3', '1', '2'])
1546         ae(filt(None, {}, [('-','messages.date')]),
1547             ['1', '2', '3', '4', '8', '5', '6', '7'])
1548         ae(filt(None, {}, [('+','messages.author'),('+','messages.date')]),
1549             ['1', '2', '3', '4', '5', '8', '6', '7'])
1550         ae(filt(None, {}, [('-','messages.author'),('+','messages.date')]),
1551             ['8', '6', '7', '5', '4', '3', '1', '2'])
1552         ae(filt(None, {}, [('+','messages.author'),('-','messages.date')]),
1553             ['1', '2', '3', '4', '5', '8', '6', '7'])
1554         ae(filt(None, {}, [('-','messages.author'),('-','messages.date')]),
1555             ['8', '6', '7', '5', '4', '3', '1', '2'])
1556         ae(filt(None, {}, [('+','messages.author'),('+','assignedto')]),
1557             ['1', '2', '3', '4', '5', '8', '6', '7'])
1558         ae(filt(None, {}, [('+','messages.author'),
1559             ('-','assignedto.supervisor'),('-','assignedto')]),
1560             ['1', '2', '3', '4', '5', '8', '6', '7'])
1561         ae(filt(None, {},
1562             [('+','messages.author.supervisor.supervisor.supervisor'),
1563             ('+','messages.author.supervisor.supervisor'),
1564             ('+','messages.author.supervisor'), ('+','messages.author')]),
1565             ['1', '2', '3', '4', '5', '6', '7', '8'])
1566         self.db.user.setorderprop('age')
1567         self.db.msg.setorderprop('date')
1568         ae(filt(None, {}, [('+','messages'), ('+','messages.author')]),
1569             ['6', '7', '8', '5', '4', '3', '1', '2'])
1570         ae(filt(None, {}, [('+','messages.author'), ('+','messages')]),
1571             ['6', '7', '8', '5', '4', '3', '1', '2'])
1572         self.db.msg.setorderprop('author')
1573         # Orderprop is a Link/Multilink:
1574         # messages are sorted by orderprop().labelprop(), i.e. by
1575         # author.username, *not* by author.orderprop() (author.age)!
1576         ae(filt(None, {}, [('+','messages')]),
1577             ['1', '2', '3', '4', '5', '8', '6', '7'])
1578         ae(filt(None, {}, [('+','messages.author'), ('+','messages')]),
1579             ['6', '7', '8', '5', '4', '3', '1', '2'])
1580         # The following will sort by
1581         # author.supervisor.username and then by
1582         # author.username
1583         # I've resited the tempation to implement recursive orderprop
1584         # here: There could even be loops if several classes specify a
1585         # Link or Multilink as the orderprop...
1586         # msg: 4: worker1 (id  5) : grouplead1 (id 4) ceo (id 3)
1587         # msg: 5: worker2 (id  7) : grouplead1 (id 4) ceo (id 3)
1588         # msg: 6: worker3 (id  8) : grouplead2 (id 5) ceo (id 3)
1589         # msg: 7: worker4 (id  9) : grouplead2 (id 5) ceo (id 3)
1590         # msg: 8: worker5 (id 10) : grouplead2 (id 5) ceo (id 3)
1591         # issue 1: messages 4   sortkey:[[grouplead1], [worker1], 1]
1592         # issue 2: messages 4   sortkey:[[grouplead1], [worker1], 2]
1593         # issue 3: messages 5   sortkey:[[grouplead1], [worker2], 3]
1594         # issue 4: messages 6   sortkey:[[grouplead2], [worker3], 4]
1595         # issue 5: messages 7   sortkey:[[grouplead2], [worker4], 5]
1596         # issue 6: messages 8   sortkey:[[grouplead2], [worker5], 6]
1597         # issue 7: messages 8   sortkey:[[grouplead2], [worker5], 7]
1598         # issue 8: messages 7,8 sortkey:[[grouplead2, grouplead2], ...]
1599         self.db.user.setorderprop('supervisor')
1600         ae(filt(None, {}, [('+','messages.author'), ('-','messages')]),
1601             ['3', '1', '2', '6', '7', '5', '4', '8'])
1603     def testFilteringSortId(self):
1604         ae, filt = self.filteringSetupTransitiveSearch()
1605         ae(self.db.user.filter(None, {}, ('+','id')),
1606             ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'])
1608 # XXX add sorting tests for other types
1610     def testImportExport(self):
1611         # use the filtering setup to create a bunch of items
1612         ae, filt = self.filteringSetup()
1613         # Get some stuff into the journal for testing import/export of
1614         # journal data:
1615         self.db.user.set('4', password = password.Password('xyzzy'))
1616         self.db.user.set('4', age = 3)
1617         self.db.user.set('4', assignable = True)
1618         self.db.issue.set('1', title = 'i1', status = '3')
1619         self.db.issue.set('1', deadline = date.Date('2007'))
1620         self.db.issue.set('1', foo = date.Interval('1:20'))
1621         p = self.db.priority.create(name = 'some_prio_without_order')
1622         self.db.commit()
1623         self.db.user.set('4', password = password.Password('123xyzzy'))
1624         self.db.user.set('4', assignable = False)
1625         self.db.priority.set(p, order = '4711')
1626         self.db.commit()
1628         self.db.user.retire('3')
1629         self.db.issue.retire('2')
1631         # grab snapshot of the current database
1632         orig = {}
1633         origj = {}
1634         for cn,klass in self.db.classes.items():
1635             cl = orig[cn] = {}
1636             jn = origj[cn] = {}
1637             for id in klass.list():
1638                 it = cl[id] = {}
1639                 jn[id] = self.db.getjournal(cn, id)
1640                 for name in klass.getprops().keys():
1641                     it[name] = klass.get(id, name)
1643         os.mkdir('_test_export')
1644         try:
1645             # grab the export
1646             export = {}
1647             journals = {}
1648             for cn,klass in self.db.classes.items():
1649                 names = klass.export_propnames()
1650                 cl = export[cn] = [names+['is retired']]
1651                 for id in klass.getnodeids():
1652                     cl.append(klass.export_list(names, id))
1653                     if hasattr(klass, 'export_files'):
1654                         klass.export_files('_test_export', id)
1655                 journals[cn] = klass.export_journals()
1657             # shut down this db and nuke it
1658             self.db.close()
1659             self.nuke_database()
1661             # open a new, empty database
1662             os.makedirs(config.DATABASE + '/files')
1663             self.db = self.module.Database(config, 'admin')
1664             setupSchema(self.db, 0, self.module)
1666             # import
1667             for cn, items in export.items():
1668                 klass = self.db.classes[cn]
1669                 names = items[0]
1670                 maxid = 1
1671                 for itemprops in items[1:]:
1672                     id = int(klass.import_list(names, itemprops))
1673                     if hasattr(klass, 'import_files'):
1674                         klass.import_files('_test_export', str(id))
1675                     maxid = max(maxid, id)
1676                 self.db.setid(cn, str(maxid+1))
1677                 klass.import_journals(journals[cn])
1678             # This is needed, otherwise journals won't be there for anydbm
1679             self.db.commit()
1680         finally:
1681             shutil.rmtree('_test_export')
1683         # compare with snapshot of the database
1684         for cn, items in orig.iteritems():
1685             klass = self.db.classes[cn]
1686             propdefs = klass.getprops(1)
1687             # ensure retired items are retired :)
1688             l = items.keys(); l.sort()
1689             m = klass.list(); m.sort()
1690             ae(l, m, '%s id list wrong %r vs. %r'%(cn, l, m))
1691             for id, props in items.items():
1692                 for name, value in props.items():
1693                     l = klass.get(id, name)
1694                     if isinstance(value, type([])):
1695                         value.sort()
1696                         l.sort()
1697                     try:
1698                         ae(l, value)
1699                     except AssertionError:
1700                         if not isinstance(propdefs[name], Date):
1701                             raise
1702                         # don't get hung up on rounding errors
1703                         assert not l.__cmp__(value, int_seconds=1)
1704         for jc, items in origj.iteritems():
1705             for id, oj in items.iteritems():
1706                 rj = self.db.getjournal(jc, id)
1707                 # Both mysql and postgresql have some minor issues with
1708                 # rounded seconds on export/import, so we compare only
1709                 # the integer part.
1710                 for j in oj:
1711                     j[1].second = float(int(j[1].second))
1712                 for j in rj:
1713                     j[1].second = float(int(j[1].second))
1714                 oj.sort()
1715                 rj.sort()
1716                 ae(oj, rj)
1718         # make sure the retired items are actually imported
1719         ae(self.db.user.get('4', 'username'), 'blop')
1720         ae(self.db.issue.get('2', 'title'), 'issue two')
1722         # make sure id counters are set correctly
1723         maxid = max([int(id) for id in self.db.user.list()])
1724         newid = self.db.user.create(username='testing')
1725         assert newid > maxid
1727     def testAddProperty(self):
1728         self.db.issue.create(title="spam", status='1')
1729         self.db.commit()
1731         self.db.issue.addprop(fixer=Link("user"))
1732         # force any post-init stuff to happen
1733         self.db.post_init()
1734         props = self.db.issue.getprops()
1735         keys = props.keys()
1736         keys.sort()
1737         self.assertEqual(keys, ['activity', 'actor', 'assignedto', 'creation',
1738             'creator', 'deadline', 'feedback', 'files', 'fixer', 'foo', 'id', 'messages',
1739             'nosy', 'priority', 'spam', 'status', 'superseder', 'title'])
1740         self.assertEqual(self.db.issue.get('1', "fixer"), None)
1742     def testRemoveProperty(self):
1743         self.db.issue.create(title="spam", status='1')
1744         self.db.commit()
1746         del self.db.issue.properties['title']
1747         self.db.post_init()
1748         props = self.db.issue.getprops()
1749         keys = props.keys()
1750         keys.sort()
1751         self.assertEqual(keys, ['activity', 'actor', 'assignedto', 'creation',
1752             'creator', 'deadline', 'feedback', 'files', 'foo', 'id', 'messages',
1753             'nosy', 'priority', 'spam', 'status', 'superseder'])
1754         self.assertEqual(self.db.issue.list(), ['1'])
1756     def testAddRemoveProperty(self):
1757         self.db.issue.create(title="spam", status='1')
1758         self.db.commit()
1760         self.db.issue.addprop(fixer=Link("user"))
1761         del self.db.issue.properties['title']
1762         self.db.post_init()
1763         props = self.db.issue.getprops()
1764         keys = props.keys()
1765         keys.sort()
1766         self.assertEqual(keys, ['activity', 'actor', 'assignedto', 'creation',
1767             'creator', 'deadline', 'feedback', 'files', 'fixer', 'foo', 'id',
1768             'messages', 'nosy', 'priority', 'spam', 'status', 'superseder'])
1769         self.assertEqual(self.db.issue.list(), ['1'])
1771     def testNosyMail(self) :
1772         """Creates one issue with two attachments, one smaller and one larger
1773            than the set max_attachment_size.
1774         """
1775         db = self.db
1776         db.config.NOSY_MAX_ATTACHMENT_SIZE = 4096
1777         res = dict(mail_to = None, mail_msg = None)
1778         def dummy_snd(s, to, msg, res=res) :
1779             res["mail_to"], res["mail_msg"] = to, msg
1780         backup, Mailer.smtp_send = Mailer.smtp_send, dummy_snd
1781         try :
1782             f1 = db.file.create(name="test1.txt", content="x" * 20)
1783             f2 = db.file.create(name="test2.txt", content="y" * 5000)
1784             m  = db.msg.create(content="one two", author="admin",
1785                 files = [f1, f2])
1786             i  = db.issue.create(title='spam', files = [f1, f2],
1787                 messages = [m], nosy = [db.user.lookup("fred")])
1789             db.issue.nosymessage(i, m, {})
1790             mail_msg = res["mail_msg"].getvalue()
1791             self.assertEqual(res["mail_to"], ["fred@example.com"])
1792             self.failUnless("From: admin" in mail_msg)
1793             self.failUnless("Subject: [issue1] spam" in mail_msg)
1794             self.failUnless("New submission from admin" in mail_msg)
1795             self.failUnless("one two" in mail_msg)
1796             self.failIf("File 'test1.txt' not attached" in mail_msg)
1797             self.failUnless(base64.encodestring("xxx").rstrip() in mail_msg)
1798             self.failUnless("File 'test2.txt' not attached" in mail_msg)
1799             self.failIf(base64.encodestring("yyy").rstrip() in mail_msg)
1800         finally :
1801             Mailer.smtp_send = backup
1803 class ROTest(MyTestCase):
1804     def setUp(self):
1805         # remove previous test, ignore errors
1806         if os.path.exists(config.DATABASE):
1807             shutil.rmtree(config.DATABASE)
1808         os.makedirs(config.DATABASE + '/files')
1809         self.db = self.module.Database(config, 'admin')
1810         setupSchema(self.db, 1, self.module)
1811         self.db.close()
1813         self.db = self.module.Database(config)
1814         setupSchema(self.db, 0, self.module)
1816     def testExceptions(self):
1817         # this tests the exceptions that should be raised
1818         ar = self.assertRaises
1820         # this tests the exceptions that should be raised
1821         ar(DatabaseError, self.db.status.create, name="foo")
1822         ar(DatabaseError, self.db.status.set, '1', name="foo")
1823         ar(DatabaseError, self.db.status.retire, '1')
1826 class SchemaTest(MyTestCase):
1827     def setUp(self):
1828         # remove previous test, ignore errors
1829         if os.path.exists(config.DATABASE):
1830             shutil.rmtree(config.DATABASE)
1831         os.makedirs(config.DATABASE + '/files')
1833     def test_reservedProperties(self):
1834         self.db = self.module.Database(config, 'admin')
1835         self.assertRaises(ValueError, self.module.Class, self.db, "a",
1836             creation=String())
1837         self.assertRaises(ValueError, self.module.Class, self.db, "a",
1838             activity=String())
1839         self.assertRaises(ValueError, self.module.Class, self.db, "a",
1840             creator=String())
1841         self.assertRaises(ValueError, self.module.Class, self.db, "a",
1842             actor=String())
1844     def init_a(self):
1845         self.db = self.module.Database(config, 'admin')
1846         a = self.module.Class(self.db, "a", name=String())
1847         a.setkey("name")
1848         self.db.post_init()
1850     def test_fileClassProps(self):
1851         self.db = self.module.Database(config, 'admin')
1852         a = self.module.FileClass(self.db, 'a')
1853         l = a.getprops().keys()
1854         l.sort()
1855         self.assert_(l, ['activity', 'actor', 'content', 'created',
1856             'creation', 'type'])
1858     def init_ab(self):
1859         self.db = self.module.Database(config, 'admin')
1860         a = self.module.Class(self.db, "a", name=String())
1861         a.setkey("name")
1862         b = self.module.Class(self.db, "b", name=String(),
1863             fooz=Multilink('a'))
1864         b.setkey("name")
1865         self.db.post_init()
1867     def test_addNewClass(self):
1868         self.init_a()
1870         self.assertRaises(ValueError, self.module.Class, self.db, "a",
1871             name=String())
1873         aid = self.db.a.create(name='apple')
1874         self.db.commit(); self.db.close()
1876         # add a new class to the schema and check creation of new items
1877         # (and existence of old ones)
1878         self.init_ab()
1879         bid = self.db.b.create(name='bear', fooz=[aid])
1880         self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
1881         self.db.commit()
1882         self.db.close()
1884         # now check we can recall the added class' items
1885         self.init_ab()
1886         self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
1887         self.assertEqual(self.db.a.lookup('apple'), aid)
1888         self.assertEqual(self.db.b.get(bid, 'name'), 'bear')
1889         self.assertEqual(self.db.b.get(bid, 'fooz'), [aid])
1890         self.assertEqual(self.db.b.lookup('bear'), bid)
1892         # confirm journal's ok
1893         self.db.getjournal('a', aid)
1894         self.db.getjournal('b', bid)
1896     def init_amod(self):
1897         self.db = self.module.Database(config, 'admin')
1898         a = self.module.Class(self.db, "a", name=String(), newstr=String(),
1899             newint=Interval(), newnum=Number(), newbool=Boolean(),
1900             newdate=Date())
1901         a.setkey("name")
1902         b = self.module.Class(self.db, "b", name=String())
1903         b.setkey("name")
1904         self.db.post_init()
1906     def test_modifyClass(self):
1907         self.init_ab()
1909         # add item to user and issue class
1910         aid = self.db.a.create(name='apple')
1911         bid = self.db.b.create(name='bear')
1912         self.db.commit(); self.db.close()
1914         # modify "a" schema
1915         self.init_amod()
1916         self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
1917         self.assertEqual(self.db.a.get(aid, 'newstr'), None)
1918         self.assertEqual(self.db.a.get(aid, 'newint'), None)
1919         # hack - metakit can't return None for missing values, and we're not
1920         # really checking for that behavior here anyway
1921         self.assert_(not self.db.a.get(aid, 'newnum'))
1922         self.assert_(not self.db.a.get(aid, 'newbool'))
1923         self.assertEqual(self.db.a.get(aid, 'newdate'), None)
1924         self.assertEqual(self.db.b.get(aid, 'name'), 'bear')
1925         aid2 = self.db.a.create(name='aardvark', newstr='booz')
1926         self.db.commit(); self.db.close()
1928         # test
1929         self.init_amod()
1930         self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
1931         self.assertEqual(self.db.a.get(aid, 'newstr'), None)
1932         self.assertEqual(self.db.b.get(aid, 'name'), 'bear')
1933         self.assertEqual(self.db.a.get(aid2, 'name'), 'aardvark')
1934         self.assertEqual(self.db.a.get(aid2, 'newstr'), 'booz')
1936         # confirm journal's ok
1937         self.db.getjournal('a', aid)
1938         self.db.getjournal('a', aid2)
1940     def init_amodkey(self):
1941         self.db = self.module.Database(config, 'admin')
1942         a = self.module.Class(self.db, "a", name=String(), newstr=String())
1943         a.setkey("newstr")
1944         b = self.module.Class(self.db, "b", name=String())
1945         b.setkey("name")
1946         self.db.post_init()
1948     def test_changeClassKey(self):
1949         self.init_amod()
1950         aid = self.db.a.create(name='apple')
1951         self.assertEqual(self.db.a.lookup('apple'), aid)
1952         self.db.commit(); self.db.close()
1954         # change the key to newstr on a
1955         self.init_amodkey()
1956         self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
1957         self.assertEqual(self.db.a.get(aid, 'newstr'), None)
1958         self.assertRaises(KeyError, self.db.a.lookup, 'apple')
1959         aid2 = self.db.a.create(name='aardvark', newstr='booz')
1960         self.db.commit(); self.db.close()
1962         # check
1963         self.init_amodkey()
1964         self.assertEqual(self.db.a.lookup('booz'), aid2)
1966         # confirm journal's ok
1967         self.db.getjournal('a', aid)
1969     def test_removeClassKey(self):
1970         self.init_amod()
1971         aid = self.db.a.create(name='apple')
1972         self.assertEqual(self.db.a.lookup('apple'), aid)
1973         self.db.commit(); self.db.close()
1975         self.db = self.module.Database(config, 'admin')
1976         a = self.module.Class(self.db, "a", name=String(), newstr=String())
1977         self.db.post_init()
1979         aid2 = self.db.a.create(name='apple', newstr='booz')
1980         self.db.commit()
1983     def init_amodml(self):
1984         self.db = self.module.Database(config, 'admin')
1985         a = self.module.Class(self.db, "a", name=String(),
1986             newml=Multilink('a'))
1987         a.setkey('name')
1988         self.db.post_init()
1990     def test_makeNewMultilink(self):
1991         self.init_a()
1992         aid = self.db.a.create(name='apple')
1993         self.assertEqual(self.db.a.lookup('apple'), aid)
1994         self.db.commit(); self.db.close()
1996         # add a multilink prop
1997         self.init_amodml()
1998         bid = self.db.a.create(name='bear', newml=[aid])
1999         self.assertEqual(self.db.a.find(newml=aid), [bid])
2000         self.assertEqual(self.db.a.lookup('apple'), aid)
2001         self.db.commit(); self.db.close()
2003         # check
2004         self.init_amodml()
2005         self.assertEqual(self.db.a.find(newml=aid), [bid])
2006         self.assertEqual(self.db.a.lookup('apple'), aid)
2007         self.assertEqual(self.db.a.lookup('bear'), bid)
2009         # confirm journal's ok
2010         self.db.getjournal('a', aid)
2011         self.db.getjournal('a', bid)
2013     def test_removeMultilink(self):
2014         # add a multilink prop
2015         self.init_amodml()
2016         aid = self.db.a.create(name='apple')
2017         bid = self.db.a.create(name='bear', newml=[aid])
2018         self.assertEqual(self.db.a.find(newml=aid), [bid])
2019         self.assertEqual(self.db.a.lookup('apple'), aid)
2020         self.assertEqual(self.db.a.lookup('bear'), bid)
2021         self.db.commit(); self.db.close()
2023         # remove the multilink
2024         self.init_a()
2025         self.assertEqual(self.db.a.lookup('apple'), aid)
2026         self.assertEqual(self.db.a.lookup('bear'), bid)
2028         # confirm journal's ok
2029         self.db.getjournal('a', aid)
2030         self.db.getjournal('a', bid)
2032     def test_removeClass(self):
2033         self.init_ab()
2034         aid = self.db.a.create(name='apple')
2035         bid = self.db.b.create(name='bear')
2036         self.db.commit(); self.db.close()
2038         # drop the b class
2039         self.init_a()
2040         self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
2041         self.assertEqual(self.db.a.lookup('apple'), aid)
2042         self.db.commit(); self.db.close()
2044         # now check we can recall the added class' items
2045         self.init_a()
2046         self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
2047         self.assertEqual(self.db.a.lookup('apple'), aid)
2049         # confirm journal's ok
2050         self.db.getjournal('a', aid)
2052 class RDBMSTest:
2053     ''' tests specific to RDBMS backends '''
2054     def test_indexTest(self):
2055         self.assertEqual(self.db.sql_index_exists('_issue', '_issue_id_idx'), 1)
2056         self.assertEqual(self.db.sql_index_exists('_issue', '_issue_x_idx'), 0)
2059 class ClassicInitTest(unittest.TestCase):
2060     count = 0
2061     db = None
2063     def setUp(self):
2064         ClassicInitTest.count = ClassicInitTest.count + 1
2065         self.dirname = '_test_init_%s'%self.count
2066         try:
2067             shutil.rmtree(self.dirname)
2068         except OSError, error:
2069             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
2071     def testCreation(self):
2072         ae = self.assertEqual
2074         # set up and open a tracker
2075         tracker = setupTracker(self.dirname, self.backend)
2076         # open the database
2077         db = self.db = tracker.open('test')
2079         # check the basics of the schema and initial data set
2080         l = db.priority.list()
2081         l.sort()
2082         ae(l, ['1', '2', '3', '4', '5'])
2083         l = db.status.list()
2084         l.sort()
2085         ae(l, ['1', '2', '3', '4', '5', '6', '7', '8'])
2086         l = db.keyword.list()
2087         ae(l, [])
2088         l = db.user.list()
2089         l.sort()
2090         ae(l, ['1', '2'])
2091         l = db.msg.list()
2092         ae(l, [])
2093         l = db.file.list()
2094         ae(l, [])
2095         l = db.issue.list()
2096         ae(l, [])
2098     def tearDown(self):
2099         if self.db is not None:
2100             self.db.close()
2101         try:
2102             shutil.rmtree(self.dirname)
2103         except OSError, error:
2104             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
2106 # vim: set et sts=4 sw=4 :