Code

428bc1d5e6b287eb8066cbd397386b57ee766777
[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, base64, os.path
21 # Python 2.3 ... 2.6 compatibility:
22 from roundup.anypy.sets_ import set
24 from roundup.hyperdb import String, Password, Link, Multilink, Date, \
25     Interval, DatabaseError, Boolean, Number, Node
26 from roundup.mailer import Mailer
27 from roundup import date, password, init, instance, configuration, \
28     roundupdb, i18n
30 from mocknull import MockNull
32 config = configuration.CoreConfig()
33 config.DATABASE = "db"
34 config.RDBMS_NAME = "rounduptest"
35 config.RDBMS_HOST = "localhost"
36 config.RDBMS_USER = "rounduptest"
37 config.RDBMS_PASSWORD = "rounduptest"
38 config.RDBMS_TEMPLATE = "template0"
39 #config.logging = MockNull()
40 # these TRACKER_WEB and MAIL_DOMAIN values are used in mailgw tests
41 config.MAIL_DOMAIN = "your.tracker.email.domain.example"
42 config.TRACKER_WEB = "http://tracker.example/cgi-bin/roundup.cgi/bugs/"
43 # uncomment the following to have excessive debug output from test cases
44 # FIXME: tracker logging level should be increased by -v arguments
45 #   to 'run_tests.py' script
46 #config.LOGGING_FILENAME = "/tmp/logfile"
47 #config.LOGGING_LEVEL = "DEBUG"
48 config.init_logging()
50 def setupTracker(dirname, backend="anydbm"):
51     """Install and initialize new tracker in dirname; return tracker instance.
53     If the directory exists, it is wiped out before the operation.
55     """
56     global config
57     try:
58         shutil.rmtree(dirname)
59     except OSError, error:
60         if error.errno not in (errno.ENOENT, errno.ESRCH): raise
61     # create the instance
62     init.install(dirname, os.path.join(os.path.dirname(__file__),
63                                        '..',
64                                        'share',
65                                        'roundup',
66                                        'templates',
67                                        'classic'))
68     init.write_select_db(dirname, backend)
69     config.save(os.path.join(dirname, 'config.ini'))
70     tracker = instance.open(dirname)
71     if tracker.exists():
72         tracker.nuke()
73         init.write_select_db(dirname, backend)
74     tracker.init(password.Password('sekrit'))
75     return tracker
77 def setupSchema(db, create, module):
78     status = module.Class(db, "status", name=String())
79     status.setkey("name")
80     priority = module.Class(db, "priority", name=String(), order=String())
81     priority.setkey("name")
82     user = module.Class(db, "user", username=String(), password=Password(),
83         assignable=Boolean(), age=Number(), roles=String(), address=String(),
84         supervisor=Link('user'),realname=String())
85     user.setkey("username")
86     file = module.FileClass(db, "file", name=String(), type=String(),
87         comment=String(indexme="yes"), fooz=Password())
88     file_nidx = module.FileClass(db, "file_nidx", content=String(indexme='no'))
89     issue = module.IssueClass(db, "issue", title=String(indexme="yes"),
90         status=Link("status"), nosy=Multilink("user"), deadline=Date(),
91         foo=Interval(), files=Multilink("file"), assignedto=Link('user'),
92         priority=Link('priority'), spam=Multilink('msg'),
93         feedback=Link('msg'))
94     stuff = module.Class(db, "stuff", stuff=String())
95     session = module.Class(db, 'session', title=String())
96     msg = module.FileClass(db, "msg", date=Date(),
97                            author=Link("user", do_journal='no'),
98                            files=Multilink('file'), inreplyto=String(),
99                            messageid=String(),
100                            recipients=Multilink("user", do_journal='no')
101                            )
102     session.disableJournalling()
103     db.post_init()
104     if create:
105         user.create(username="admin", roles='Admin',
106             password=password.Password('sekrit'))
107         user.create(username="fred", roles='User',
108             password=password.Password('sekrit'), address='fred@example.com')
109         status.create(name="unread")
110         status.create(name="in-progress")
111         status.create(name="testing")
112         status.create(name="resolved")
113         priority.create(name="feature", order="2")
114         priority.create(name="wish", order="3")
115         priority.create(name="bug", order="1")
116     db.commit()
118     # nosy tests require this
119     db.security.addPermissionToRole('User', 'View', 'msg')
121 class MyTestCase(unittest.TestCase):
122     def tearDown(self):
123         if hasattr(self, 'db'):
124             self.db.close()
125         if os.path.exists(config.DATABASE):
126             shutil.rmtree(config.DATABASE)
128     def open_database(self):
129         self.db = self.module.Database(config, 'admin')
132 if os.environ.has_key('LOGGING_LEVEL'):
133     from roundup import rlog
134     config.logging = rlog.BasicLogging()
135     config.logging.setLevel(os.environ['LOGGING_LEVEL'])
136     config.logging.getLogger('roundup.hyperdb').setFormat('%(message)s')
138 class commonDBTest(MyTestCase):
139     def setUp(self):
140         # remove previous test, ignore errors
141         if os.path.exists(config.DATABASE):
142             shutil.rmtree(config.DATABASE)
143         os.makedirs(config.DATABASE + '/files')
144         self.open_database()
145         setupSchema(self.db, 1, self.module)
147     def iterSetup(self, classname='issue'):
148         cls = getattr(self.db, classname)
149         def filt_iter(*args):
150             """ for checking equivalence of filter and filter_iter """
151             return list(cls.filter_iter(*args))
152         return self.assertEqual, cls.filter, filt_iter
154     def filteringSetupTransitiveSearch(self, classname='issue'):
155         u_m = {}
156         k = 30
157         for user in (
158                 {'username': 'ceo', 'age': 129},
159                 {'username': 'grouplead1', 'age': 29, 'supervisor': '3'},
160                 {'username': 'grouplead2', 'age': 29, 'supervisor': '3'},
161                 {'username': 'worker1', 'age': 25, 'supervisor' : '4'},
162                 {'username': 'worker2', 'age': 24, 'supervisor' : '4'},
163                 {'username': 'worker3', 'age': 23, 'supervisor' : '5'},
164                 {'username': 'worker4', 'age': 22, 'supervisor' : '5'},
165                 {'username': 'worker5', 'age': 21, 'supervisor' : '5'}):
166             u = self.db.user.create(**user)
167             u_m [u] = self.db.msg.create(author = u, content = ' '
168                 , date = date.Date ('2006-01-%s' % k))
169             k -= 1
170         i = date.Interval('-1d')
171         for issue in (
172                 {'title': 'ts1', 'status': '2', 'assignedto': '6',
173                     'priority': '3', 'messages' : [u_m ['6']], 'nosy' : ['4']},
174                 {'title': 'ts2', 'status': '1', 'assignedto': '6',
175                     'priority': '3', 'messages' : [u_m ['6']], 'nosy' : ['5']},
176                 {'title': 'ts4', 'status': '2', 'assignedto': '7',
177                     'priority': '3', 'messages' : [u_m ['7']]},
178                 {'title': 'ts5', 'status': '1', 'assignedto': '8',
179                     'priority': '3', 'messages' : [u_m ['8']]},
180                 {'title': 'ts6', 'status': '2', 'assignedto': '9',
181                     'priority': '3', 'messages' : [u_m ['9']]},
182                 {'title': 'ts7', 'status': '1', 'assignedto': '10',
183                     'priority': '3', 'messages' : [u_m ['10']]},
184                 {'title': 'ts8', 'status': '2', 'assignedto': '10',
185                     'priority': '3', 'messages' : [u_m ['10']], 'foo' : i},
186                 {'title': 'ts9', 'status': '1', 'assignedto': '10',
187                     'priority': '3', 'messages' : [u_m ['10'], u_m ['9']]}):
188             self.db.issue.create(**issue)
189         return self.iterSetup(classname)
192 class DBTest(commonDBTest):
194     def testRefresh(self):
195         self.db.refresh_database()
197     #
198     # automatic properties (well, the two easy ones anyway)
199     #
200     def testCreatorProperty(self):
201         i = self.db.issue
202         id1 = i.create(title='spam')
203         self.db.journaltag = 'fred'
204         id2 = i.create(title='spam')
205         self.assertNotEqual(id1, id2)
206         self.assertNotEqual(i.get(id1, 'creator'), i.get(id2, 'creator'))
208     def testActorProperty(self):
209         i = self.db.issue
210         id1 = i.create(title='spam')
211         self.db.journaltag = 'fred'
212         i.set(id1, title='asfasd')
213         self.assertNotEqual(i.get(id1, 'creator'), i.get(id1, 'actor'))
215     # ID number controls
216     def testIDGeneration(self):
217         id1 = self.db.issue.create(title="spam", status='1')
218         id2 = self.db.issue.create(title="eggs", status='2')
219         self.assertNotEqual(id1, id2)
220     def testIDSetting(self):
221         # XXX numeric ids
222         self.db.setid('issue', 10)
223         id2 = self.db.issue.create(title="eggs", status='2')
224         self.assertEqual('11', id2)
226     #
227     # basic operations
228     #
229     def testEmptySet(self):
230         id1 = self.db.issue.create(title="spam", status='1')
231         self.db.issue.set(id1)
233     # String
234     def testStringChange(self):
235         for commit in (0,1):
236             # test set & retrieve
237             nid = self.db.issue.create(title="spam", status='1')
238             self.assertEqual(self.db.issue.get(nid, 'title'), 'spam')
240             # change and make sure we retrieve the correct value
241             self.db.issue.set(nid, title='eggs')
242             if commit: self.db.commit()
243             self.assertEqual(self.db.issue.get(nid, 'title'), 'eggs')
245     def testStringUnset(self):
246         for commit in (0,1):
247             nid = self.db.issue.create(title="spam", status='1')
248             if commit: self.db.commit()
249             self.assertEqual(self.db.issue.get(nid, 'title'), 'spam')
250             # make sure we can unset
251             self.db.issue.set(nid, title=None)
252             if commit: self.db.commit()
253             self.assertEqual(self.db.issue.get(nid, "title"), None)
255     # FileClass "content" property (no unset test)
256     def testFileClassContentChange(self):
257         for commit in (0,1):
258             # test set & retrieve
259             nid = self.db.file.create(content="spam")
260             self.assertEqual(self.db.file.get(nid, 'content'), 'spam')
262             # change and make sure we retrieve the correct value
263             self.db.file.set(nid, content='eggs')
264             if commit: self.db.commit()
265             self.assertEqual(self.db.file.get(nid, 'content'), 'eggs')
267     def testStringUnicode(self):
268         # test set & retrieve
269         ustr = u'\xe4\xf6\xfc\u20ac'.encode('utf8')
270         nid = self.db.issue.create(title=ustr, status='1')
271         self.assertEqual(self.db.issue.get(nid, 'title'), ustr)
273         # change and make sure we retrieve the correct value
274         ustr2 = u'change \u20ac change'.encode('utf8')
275         self.db.issue.set(nid, title=ustr2)
276         self.db.commit()
277         self.assertEqual(self.db.issue.get(nid, 'title'), ustr2)
279     # Link
280     def testLinkChange(self):
281         self.assertRaises(IndexError, self.db.issue.create, title="spam",
282             status='100')
283         for commit in (0,1):
284             nid = self.db.issue.create(title="spam", status='1')
285             if commit: self.db.commit()
286             self.assertEqual(self.db.issue.get(nid, "status"), '1')
287             self.db.issue.set(nid, status='2')
288             if commit: self.db.commit()
289             self.assertEqual(self.db.issue.get(nid, "status"), '2')
291     def testLinkUnset(self):
292         for commit in (0,1):
293             nid = self.db.issue.create(title="spam", status='1')
294             if commit: self.db.commit()
295             self.db.issue.set(nid, status=None)
296             if commit: self.db.commit()
297             self.assertEqual(self.db.issue.get(nid, "status"), None)
299     # Multilink
300     def testMultilinkChange(self):
301         for commit in (0,1):
302             self.assertRaises(IndexError, self.db.issue.create, title="spam",
303                 nosy=['foo%s'%commit])
304             u1 = self.db.user.create(username='foo%s'%commit)
305             u2 = self.db.user.create(username='bar%s'%commit)
306             nid = self.db.issue.create(title="spam", nosy=[u1])
307             if commit: self.db.commit()
308             self.assertEqual(self.db.issue.get(nid, "nosy"), [u1])
309             self.db.issue.set(nid, nosy=[])
310             if commit: self.db.commit()
311             self.assertEqual(self.db.issue.get(nid, "nosy"), [])
312             self.db.issue.set(nid, nosy=[u1,u2])
313             if commit: self.db.commit()
314             l = [u1,u2]; l.sort()
315             m = self.db.issue.get(nid, "nosy"); m.sort()
316             self.assertEqual(l, m)
318             # verify that when we pass None to an Multilink it sets
319             # it to an empty list
320             self.db.issue.set(nid, nosy=None)
321             if commit: self.db.commit()
322             self.assertEqual(self.db.issue.get(nid, "nosy"), [])
324     def testMakeSeveralMultilinkedNodes(self):
325         for commit in (0,1):
326             u1 = self.db.user.create(username='foo%s'%commit)
327             u2 = self.db.user.create(username='bar%s'%commit)
328             u3 = self.db.user.create(username='baz%s'%commit)
329             nid = self.db.issue.create(title="spam", nosy=[u1])
330             if commit: self.db.commit()
331             self.assertEqual(self.db.issue.get(nid, "nosy"), [u1])
332             self.db.issue.set(nid, deadline=date.Date('.'))
333             self.db.issue.set(nid, nosy=[u1,u2], title='ta%s'%commit)
334             if commit: self.db.commit()
335             self.assertEqual(self.db.issue.get(nid, "nosy"), [u1,u2])
336             self.db.issue.set(nid, deadline=date.Date('.'))
337             self.db.issue.set(nid, nosy=[u1,u2,u3], title='tb%s'%commit)
338             if commit: self.db.commit()
339             self.assertEqual(self.db.issue.get(nid, "nosy"), [u1,u2,u3])
341     def testMultilinkChangeIterable(self):
342         for commit in (0,1):
343             # invalid nosy value assertion
344             self.assertRaises(IndexError, self.db.issue.create, title='spam',
345                 nosy=['foo%s'%commit])
346             # invalid type for nosy create
347             self.assertRaises(TypeError, self.db.issue.create, title='spam',
348                 nosy=1)
349             u1 = self.db.user.create(username='foo%s'%commit)
350             u2 = self.db.user.create(username='bar%s'%commit)
351             # try a couple of the built-in iterable types to make
352             # sure that we accept them and handle them properly
353             # try a set as input for the multilink
354             nid = self.db.issue.create(title="spam", nosy=set(u1))
355             if commit: self.db.commit()
356             self.assertEqual(self.db.issue.get(nid, "nosy"), [u1])
357             self.assertRaises(TypeError, self.db.issue.set, nid,
358                 nosy='invalid type')
359             # test with a tuple
360             self.db.issue.set(nid, nosy=tuple())
361             if commit: self.db.commit()
362             self.assertEqual(self.db.issue.get(nid, "nosy"), [])
363             # make sure we accept a frozen set
364             self.db.issue.set(nid, nosy=set([u1,u2]))
365             if commit: self.db.commit()
366             l = [u1,u2]; l.sort()
367             m = self.db.issue.get(nid, "nosy"); m.sort()
368             self.assertEqual(l, m)
371 # XXX one day, maybe...
372 #    def testMultilinkOrdering(self):
373 #        for i in range(10):
374 #            self.db.user.create(username='foo%s'%i)
375 #        i = self.db.issue.create(title="spam", nosy=['5','3','12','4'])
376 #        self.db.commit()
377 #        l = self.db.issue.get(i, "nosy")
378 #        # all backends should return the Multilink numeric-id-sorted
379 #        self.assertEqual(l, ['3', '4', '5', '12'])
381     # Date
382     def testDateChange(self):
383         self.assertRaises(TypeError, self.db.issue.create,
384             title='spam', deadline=1)
385         for commit in (0,1):
386             nid = self.db.issue.create(title="spam", status='1')
387             self.assertRaises(TypeError, self.db.issue.set, nid, deadline=1)
388             a = self.db.issue.get(nid, "deadline")
389             if commit: self.db.commit()
390             self.db.issue.set(nid, deadline=date.Date())
391             b = self.db.issue.get(nid, "deadline")
392             if commit: self.db.commit()
393             self.assertNotEqual(a, b)
394             self.assertNotEqual(b, date.Date('1970-1-1.00:00:00'))
395             # The 1970 date will fail for metakit -- it is used
396             # internally for storing NULL. The others would, too
397             # because metakit tries to convert date.timestamp to an int
398             # for storing and fails with an overflow.
399             for d in [date.Date (x) for x in '2038', '1970', '0033', '9999']:
400                 self.db.issue.set(nid, deadline=d)
401                 if commit: self.db.commit()
402                 c = self.db.issue.get(nid, "deadline")
403                 self.assertEqual(c, d)
405     def testDateLeapYear(self):
406         nid = self.db.issue.create(title='spam', status='1',
407             deadline=date.Date('2008-02-29'))
408         self.assertEquals(str(self.db.issue.get(nid, 'deadline')),
409             '2008-02-29.00:00:00')
410         self.assertEquals(self.db.issue.filter(None,
411             {'deadline': '2008-02-29'}), [nid])
412         self.assertEquals(list(self.db.issue.filter_iter(None,
413             {'deadline': '2008-02-29'})), [nid])
414         self.db.issue.set(nid, deadline=date.Date('2008-03-01'))
415         self.assertEquals(str(self.db.issue.get(nid, 'deadline')),
416             '2008-03-01.00:00:00')
417         self.assertEquals(self.db.issue.filter(None,
418             {'deadline': '2008-02-29'}), [])
419         self.assertEquals(list(self.db.issue.filter_iter(None,
420             {'deadline': '2008-02-29'})), [])
422     def testDateUnset(self):
423         for commit in (0,1):
424             nid = self.db.issue.create(title="spam", status='1')
425             self.db.issue.set(nid, deadline=date.Date())
426             if commit: self.db.commit()
427             self.assertNotEqual(self.db.issue.get(nid, "deadline"), None)
428             self.db.issue.set(nid, deadline=None)
429             if commit: self.db.commit()
430             self.assertEqual(self.db.issue.get(nid, "deadline"), None)
432     # Interval
433     def testIntervalChange(self):
434         self.assertRaises(TypeError, self.db.issue.create,
435             title='spam', foo=1)
436         for commit in (0,1):
437             nid = self.db.issue.create(title="spam", status='1')
438             self.assertRaises(TypeError, self.db.issue.set, nid, foo=1)
439             if commit: self.db.commit()
440             a = self.db.issue.get(nid, "foo")
441             i = date.Interval('-1d')
442             self.db.issue.set(nid, foo=i)
443             if commit: self.db.commit()
444             self.assertNotEqual(self.db.issue.get(nid, "foo"), a)
445             self.assertEqual(i, self.db.issue.get(nid, "foo"))
446             j = date.Interval('1y')
447             self.db.issue.set(nid, foo=j)
448             if commit: self.db.commit()
449             self.assertNotEqual(self.db.issue.get(nid, "foo"), i)
450             self.assertEqual(j, self.db.issue.get(nid, "foo"))
452     def testIntervalUnset(self):
453         for commit in (0,1):
454             nid = self.db.issue.create(title="spam", status='1')
455             self.db.issue.set(nid, foo=date.Interval('-1d'))
456             if commit: self.db.commit()
457             self.assertNotEqual(self.db.issue.get(nid, "foo"), None)
458             self.db.issue.set(nid, foo=None)
459             if commit: self.db.commit()
460             self.assertEqual(self.db.issue.get(nid, "foo"), None)
462     # Boolean
463     def testBooleanSet(self):
464         nid = self.db.user.create(username='one', assignable=1)
465         self.assertEqual(self.db.user.get(nid, "assignable"), 1)
466         nid = self.db.user.create(username='two', assignable=0)
467         self.assertEqual(self.db.user.get(nid, "assignable"), 0)
469     def testBooleanChange(self):
470         userid = self.db.user.create(username='foo', assignable=1)
471         self.assertEqual(1, self.db.user.get(userid, 'assignable'))
472         self.db.user.set(userid, assignable=0)
473         self.assertEqual(self.db.user.get(userid, 'assignable'), 0)
474         self.db.user.set(userid, assignable=1)
475         self.assertEqual(self.db.user.get(userid, 'assignable'), 1)
477     def testBooleanUnset(self):
478         nid = self.db.user.create(username='foo', assignable=1)
479         self.db.user.set(nid, assignable=None)
480         self.assertEqual(self.db.user.get(nid, "assignable"), None)
482     # Number
483     def testNumberChange(self):
484         nid = self.db.user.create(username='foo', age=1)
485         self.assertEqual(1, self.db.user.get(nid, 'age'))
486         self.db.user.set(nid, age=3)
487         self.assertNotEqual(self.db.user.get(nid, 'age'), 1)
488         self.db.user.set(nid, age=1.0)
489         self.assertEqual(self.db.user.get(nid, 'age'), 1)
490         self.db.user.set(nid, age=0)
491         self.assertEqual(self.db.user.get(nid, 'age'), 0)
493         nid = self.db.user.create(username='bar', age=0)
494         self.assertEqual(self.db.user.get(nid, 'age'), 0)
496     def testNumberUnset(self):
497         nid = self.db.user.create(username='foo', age=1)
498         self.db.user.set(nid, age=None)
499         self.assertEqual(self.db.user.get(nid, "age"), None)
501     # Password
502     def testPasswordChange(self):
503         x = password.Password('x')
504         userid = self.db.user.create(username='foo', password=x)
505         self.assertEqual(x, self.db.user.get(userid, 'password'))
506         self.assertEqual(self.db.user.get(userid, 'password'), 'x')
507         y = password.Password('y')
508         self.db.user.set(userid, password=y)
509         self.assertEqual(self.db.user.get(userid, 'password'), 'y')
510         self.assertRaises(TypeError, self.db.user.create, userid,
511             username='bar', password='x')
512         self.assertRaises(TypeError, self.db.user.set, userid, password='x')
514     def testPasswordUnset(self):
515         x = password.Password('x')
516         nid = self.db.user.create(username='foo', password=x)
517         self.db.user.set(nid, assignable=None)
518         self.assertEqual(self.db.user.get(nid, "assignable"), None)
520     # key value
521     def testKeyValue(self):
522         self.assertRaises(ValueError, self.db.user.create)
524         newid = self.db.user.create(username="spam")
525         self.assertEqual(self.db.user.lookup('spam'), newid)
526         self.db.commit()
527         self.assertEqual(self.db.user.lookup('spam'), newid)
528         self.db.user.retire(newid)
529         self.assertRaises(KeyError, self.db.user.lookup, 'spam')
531         # use the key again now that the old is retired
532         newid2 = self.db.user.create(username="spam")
533         self.assertNotEqual(newid, newid2)
534         # try to restore old node. this shouldn't succeed!
535         self.assertRaises(KeyError, self.db.user.restore, newid)
537         self.assertRaises(TypeError, self.db.issue.lookup, 'fubar')
539     # label property
540     def testLabelProp(self):
541         # key prop
542         self.assertEqual(self.db.status.labelprop(), 'name')
543         self.assertEqual(self.db.user.labelprop(), 'username')
544         # title
545         self.assertEqual(self.db.issue.labelprop(), 'title')
546         # name
547         self.assertEqual(self.db.file.labelprop(), 'name')
548         # id
549         self.assertEqual(self.db.stuff.labelprop(default_to_id=1), 'id')
551     # retirement
552     def testRetire(self):
553         self.db.issue.create(title="spam", status='1')
554         b = self.db.status.get('1', 'name')
555         a = self.db.status.list()
556         nodeids = self.db.status.getnodeids()
557         self.db.status.retire('1')
558         others = nodeids[:]
559         others.remove('1')
561         self.assertEqual(set(self.db.status.getnodeids()),
562             set(nodeids))
563         self.assertEqual(set(self.db.status.getnodeids(retired=True)),
564             set(['1']))
565         self.assertEqual(set(self.db.status.getnodeids(retired=False)),
566             set(others))
568         self.assert_(self.db.status.is_retired('1'))
570         # make sure the list is different
571         self.assertNotEqual(a, self.db.status.list())
573         # can still access the node if necessary
574         self.assertEqual(self.db.status.get('1', 'name'), b)
575         self.assertRaises(IndexError, self.db.status.set, '1', name='hello')
576         self.db.commit()
577         self.assert_(self.db.status.is_retired('1'))
578         self.assertEqual(self.db.status.get('1', 'name'), b)
579         self.assertNotEqual(a, self.db.status.list())
581         # try to restore retired node
582         self.db.status.restore('1')
584         self.assert_(not self.db.status.is_retired('1'))
586     def testCacheCreateSet(self):
587         self.db.issue.create(title="spam", status='1')
588         a = self.db.issue.get('1', 'title')
589         self.assertEqual(a, 'spam')
590         self.db.issue.set('1', title='ham')
591         b = self.db.issue.get('1', 'title')
592         self.assertEqual(b, 'ham')
594     def testSerialisation(self):
595         nid = self.db.issue.create(title="spam", status='1',
596             deadline=date.Date(), foo=date.Interval('-1d'))
597         self.db.commit()
598         assert isinstance(self.db.issue.get(nid, 'deadline'), date.Date)
599         assert isinstance(self.db.issue.get(nid, 'foo'), date.Interval)
600         uid = self.db.user.create(username="fozzy",
601             password=password.Password('t. bear'))
602         self.db.commit()
603         assert isinstance(self.db.user.get(uid, 'password'), password.Password)
605     def testTransactions(self):
606         # remember the number of items we started
607         num_issues = len(self.db.issue.list())
608         num_files = self.db.numfiles()
609         self.db.issue.create(title="don't commit me!", status='1')
610         self.assertNotEqual(num_issues, len(self.db.issue.list()))
611         self.db.rollback()
612         self.assertEqual(num_issues, len(self.db.issue.list()))
613         self.db.issue.create(title="please commit me!", status='1')
614         self.assertNotEqual(num_issues, len(self.db.issue.list()))
615         self.db.commit()
616         self.assertNotEqual(num_issues, len(self.db.issue.list()))
617         self.db.rollback()
618         self.assertNotEqual(num_issues, len(self.db.issue.list()))
619         self.db.file.create(name="test", type="text/plain", content="hi")
620         self.db.rollback()
621         self.assertEqual(num_files, self.db.numfiles())
622         for i in range(10):
623             self.db.file.create(name="test", type="text/plain",
624                     content="hi %d"%(i))
625             self.db.commit()
626         num_files2 = self.db.numfiles()
627         self.assertNotEqual(num_files, num_files2)
628         self.db.file.create(name="test", type="text/plain", content="hi")
629         self.db.rollback()
630         self.assertNotEqual(num_files, self.db.numfiles())
631         self.assertEqual(num_files2, self.db.numfiles())
633         # rollback / cache interaction
634         name1 = self.db.user.get('1', 'username')
635         self.db.user.set('1', username = name1+name1)
636         # get the prop so the info's forced into the cache (if there is one)
637         self.db.user.get('1', 'username')
638         self.db.rollback()
639         name2 = self.db.user.get('1', 'username')
640         self.assertEqual(name1, name2)
642     def testDestroyBlob(self):
643         # destroy an uncommitted blob
644         f1 = self.db.file.create(content='hello', type="text/plain")
645         self.db.commit()
646         fn = self.db.filename('file', f1)
647         self.db.file.destroy(f1)
648         self.db.commit()
649         self.assertEqual(os.path.exists(fn), False)
651     def testDestroyNoJournalling(self):
652         self.innerTestDestroy(klass=self.db.session)
654     def testDestroyJournalling(self):
655         self.innerTestDestroy(klass=self.db.issue)
657     def innerTestDestroy(self, klass):
658         newid = klass.create(title='Mr Friendly')
659         n = len(klass.list())
660         self.assertEqual(klass.get(newid, 'title'), 'Mr Friendly')
661         count = klass.count()
662         klass.destroy(newid)
663         self.assertNotEqual(count, klass.count())
664         self.assertRaises(IndexError, klass.get, newid, 'title')
665         self.assertNotEqual(len(klass.list()), n)
666         if klass.do_journal:
667             self.assertRaises(IndexError, klass.history, newid)
669         # now with a commit
670         newid = klass.create(title='Mr Friendly')
671         n = len(klass.list())
672         self.assertEqual(klass.get(newid, 'title'), 'Mr Friendly')
673         self.db.commit()
674         count = klass.count()
675         klass.destroy(newid)
676         self.assertNotEqual(count, klass.count())
677         self.assertRaises(IndexError, klass.get, newid, 'title')
678         self.db.commit()
679         self.assertRaises(IndexError, klass.get, newid, 'title')
680         self.assertNotEqual(len(klass.list()), n)
681         if klass.do_journal:
682             self.assertRaises(IndexError, klass.history, newid)
684         # now with a rollback
685         newid = klass.create(title='Mr Friendly')
686         n = len(klass.list())
687         self.assertEqual(klass.get(newid, 'title'), 'Mr Friendly')
688         self.db.commit()
689         count = klass.count()
690         klass.destroy(newid)
691         self.assertNotEqual(len(klass.list()), n)
692         self.assertRaises(IndexError, klass.get, newid, 'title')
693         self.db.rollback()
694         self.assertEqual(count, klass.count())
695         self.assertEqual(klass.get(newid, 'title'), 'Mr Friendly')
696         self.assertEqual(len(klass.list()), n)
697         if klass.do_journal:
698             self.assertNotEqual(klass.history(newid), [])
700     def testExceptions(self):
701         # this tests the exceptions that should be raised
702         ar = self.assertRaises
704         ar(KeyError, self.db.getclass, 'fubar')
706         #
707         # class create
708         #
709         # string property
710         ar(TypeError, self.db.status.create, name=1)
711         # id, creation, creator and activity properties are reserved
712         ar(KeyError, self.db.status.create, id=1)
713         ar(KeyError, self.db.status.create, creation=1)
714         ar(KeyError, self.db.status.create, creator=1)
715         ar(KeyError, self.db.status.create, activity=1)
716         ar(KeyError, self.db.status.create, actor=1)
717         # invalid property name
718         ar(KeyError, self.db.status.create, foo='foo')
719         # key name clash
720         ar(ValueError, self.db.status.create, name='unread')
721         # invalid link index
722         ar(IndexError, self.db.issue.create, title='foo', status='bar')
723         # invalid link value
724         ar(ValueError, self.db.issue.create, title='foo', status=1)
725         # invalid multilink type
726         ar(TypeError, self.db.issue.create, title='foo', status='1',
727             nosy='hello')
728         # invalid multilink index type
729         ar(ValueError, self.db.issue.create, title='foo', status='1',
730             nosy=[1])
731         # invalid multilink index
732         ar(IndexError, self.db.issue.create, title='foo', status='1',
733             nosy=['10'])
735         #
736         # key property
737         #
738         # key must be a String
739         ar(TypeError, self.db.file.setkey, 'fooz')
740         # key must exist
741         ar(KeyError, self.db.file.setkey, 'fubar')
743         #
744         # class get
745         #
746         # invalid node id
747         ar(IndexError, self.db.issue.get, '99', 'title')
748         # invalid property name
749         ar(KeyError, self.db.status.get, '2', 'foo')
751         #
752         # class set
753         #
754         # invalid node id
755         ar(IndexError, self.db.issue.set, '99', title='foo')
756         # invalid property name
757         ar(KeyError, self.db.status.set, '1', foo='foo')
758         # string property
759         ar(TypeError, self.db.status.set, '1', name=1)
760         # key name clash
761         ar(ValueError, self.db.status.set, '2', name='unread')
762         # set up a valid issue for me to work on
763         id = self.db.issue.create(title="spam", status='1')
764         # invalid link index
765         ar(IndexError, self.db.issue.set, id, title='foo', status='bar')
766         # invalid link value
767         ar(ValueError, self.db.issue.set, id, title='foo', status=1)
768         # invalid multilink type
769         ar(TypeError, self.db.issue.set, id, title='foo', status='1',
770             nosy='hello')
771         # invalid multilink index type
772         ar(ValueError, self.db.issue.set, id, title='foo', status='1',
773             nosy=[1])
774         # invalid multilink index
775         ar(IndexError, self.db.issue.set, id, title='foo', status='1',
776             nosy=['10'])
777         # NOTE: the following increment the username to avoid problems
778         # within metakit's backend (it creates the node, and then sets the
779         # info, so the create (and by a fluke the username set) go through
780         # before the age/assignable/etc. set, which raises the exception)
781         # invalid number value
782         ar(TypeError, self.db.user.create, username='foo', age='a')
783         # invalid boolean value
784         ar(TypeError, self.db.user.create, username='foo2', assignable='true')
785         nid = self.db.user.create(username='foo3')
786         # invalid number value
787         ar(TypeError, self.db.user.set, nid, age='a')
788         # invalid boolean value
789         ar(TypeError, self.db.user.set, nid, assignable='true')
791     def testAuditors(self):
792         class test:
793             called = False
794             def call(self, *args): self.called = True
795         create = test()
797         self.db.user.audit('create', create.call)
798         self.db.user.create(username="mary")
799         self.assertEqual(create.called, True)
801         set = test()
802         self.db.user.audit('set', set.call)
803         self.db.user.set('1', username="joe")
804         self.assertEqual(set.called, True)
806         retire = test()
807         self.db.user.audit('retire', retire.call)
808         self.db.user.retire('1')
809         self.assertEqual(retire.called, True)
811     def testAuditorTwo(self):
812         class test:
813             n = 0
814             def a(self, *args): self.call_a = self.n; self.n += 1
815             def b(self, *args): self.call_b = self.n; self.n += 1
816             def c(self, *args): self.call_c = self.n; self.n += 1
817         test = test()
818         self.db.user.audit('create', test.b, 1)
819         self.db.user.audit('create', test.a, 1)
820         self.db.user.audit('create', test.c, 2)
821         self.db.user.create(username="mary")
822         self.assertEqual(test.call_a, 0)
823         self.assertEqual(test.call_b, 1)
824         self.assertEqual(test.call_c, 2)
826     def testJournals(self):
827         muid = self.db.user.create(username="mary")
828         self.db.user.create(username="pete")
829         self.db.issue.create(title="spam", status='1')
830         self.db.commit()
832         # journal entry for issue create
833         journal = self.db.getjournal('issue', '1')
834         self.assertEqual(1, len(journal))
835         (nodeid, date_stamp, journaltag, action, params) = journal[0]
836         self.assertEqual(nodeid, '1')
837         self.assertEqual(journaltag, self.db.user.lookup('admin'))
838         self.assertEqual(action, 'create')
839         keys = params.keys()
840         keys.sort()
841         self.assertEqual(keys, [])
843         # journal entry for link
844         journal = self.db.getjournal('user', '1')
845         self.assertEqual(1, len(journal))
846         self.db.issue.set('1', assignedto='1')
847         self.db.commit()
848         journal = self.db.getjournal('user', '1')
849         self.assertEqual(2, len(journal))
850         (nodeid, date_stamp, journaltag, action, params) = journal[1]
851         self.assertEqual('1', nodeid)
852         self.assertEqual('1', journaltag)
853         self.assertEqual('link', action)
854         self.assertEqual(('issue', '1', 'assignedto'), params)
856         # wait a bit to keep proper order of journal entries
857         time.sleep(0.01)
858         # journal entry for unlink
859         self.db.setCurrentUser('mary')
860         self.db.issue.set('1', assignedto='2')
861         self.db.commit()
862         journal = self.db.getjournal('user', '1')
863         self.assertEqual(3, len(journal))
864         (nodeid, date_stamp, journaltag, action, params) = journal[2]
865         self.assertEqual('1', nodeid)
866         self.assertEqual(muid, journaltag)
867         self.assertEqual('unlink', action)
868         self.assertEqual(('issue', '1', 'assignedto'), params)
870         # test disabling journalling
871         # ... get the last entry
872         jlen = len(self.db.getjournal('user', '1'))
873         self.db.issue.disableJournalling()
874         self.db.issue.set('1', title='hello world')
875         self.db.commit()
876         # see if the change was journalled when it shouldn't have been
877         self.assertEqual(jlen,  len(self.db.getjournal('user', '1')))
878         jlen = len(self.db.getjournal('issue', '1'))
879         self.db.issue.enableJournalling()
880         self.db.issue.set('1', title='hello world 2')
881         self.db.commit()
882         # see if the change was journalled
883         self.assertNotEqual(jlen,  len(self.db.getjournal('issue', '1')))
885     def testJournalPreCommit(self):
886         id = self.db.user.create(username="mary")
887         self.assertEqual(len(self.db.getjournal('user', id)), 1)
888         self.db.commit()
890     def testPack(self):
891         id = self.db.issue.create(title="spam", status='1')
892         self.db.commit()
893         time.sleep(1)
894         self.db.issue.set(id, status='2')
895         self.db.commit()
897         # sleep for at least a second, then get a date to pack at
898         time.sleep(1)
899         pack_before = date.Date('.')
901         # wait another second and add one more entry
902         time.sleep(1)
903         self.db.issue.set(id, status='3')
904         self.db.commit()
905         jlen = len(self.db.getjournal('issue', id))
907         # pack
908         self.db.pack(pack_before)
910         # we should have the create and last set entries now
911         self.assertEqual(jlen-1, len(self.db.getjournal('issue', id)))
913     def testIndexerSearching(self):
914         f1 = self.db.file.create(content='hello', type="text/plain")
915         # content='world' has the wrong content-type and won't be indexed
916         f2 = self.db.file.create(content='world', type="text/frozz",
917             comment='blah blah')
918         i1 = self.db.issue.create(files=[f1, f2], title="flebble plop")
919         i2 = self.db.issue.create(title="flebble the frooz")
920         self.db.commit()
921         self.assertEquals(self.db.indexer.search([], self.db.issue), {})
922         self.assertEquals(self.db.indexer.search(['hello'], self.db.issue),
923             {i1: {'files': [f1]}})
924         # content='world' has the wrong content-type and shouldn't be indexed
925         self.assertEquals(self.db.indexer.search(['world'], self.db.issue), {})
926         self.assertEquals(self.db.indexer.search(['frooz'], self.db.issue),
927             {i2: {}})
928         self.assertEquals(self.db.indexer.search(['flebble'], self.db.issue),
929             {i1: {}, i2: {}})
931         # test AND'ing of search terms
932         self.assertEquals(self.db.indexer.search(['frooz', 'flebble'],
933             self.db.issue), {i2: {}})
935         # unindexed stopword
936         self.assertEquals(self.db.indexer.search(['the'], self.db.issue), {})
938     def testIndexerSearchingLink(self):
939         m1 = self.db.msg.create(content="one two")
940         i1 = self.db.issue.create(messages=[m1])
941         m2 = self.db.msg.create(content="two three")
942         i2 = self.db.issue.create(feedback=m2)
943         self.db.commit()
944         self.assertEquals(self.db.indexer.search(['two'], self.db.issue),
945             {i1: {'messages': [m1]}, i2: {'feedback': [m2]}})
947     def testIndexerSearchMulti(self):
948         m1 = self.db.msg.create(content="one two")
949         m2 = self.db.msg.create(content="two three")
950         i1 = self.db.issue.create(messages=[m1])
951         i2 = self.db.issue.create(spam=[m2])
952         self.db.commit()
953         self.assertEquals(self.db.indexer.search([], self.db.issue), {})
954         self.assertEquals(self.db.indexer.search(['one'], self.db.issue),
955             {i1: {'messages': [m1]}})
956         self.assertEquals(self.db.indexer.search(['two'], self.db.issue),
957             {i1: {'messages': [m1]}, i2: {'spam': [m2]}})
958         self.assertEquals(self.db.indexer.search(['three'], self.db.issue),
959             {i2: {'spam': [m2]}})
961     def testReindexingChange(self):
962         search = self.db.indexer.search
963         issue = self.db.issue
964         i1 = issue.create(title="flebble plop")
965         i2 = issue.create(title="flebble frooz")
966         self.db.commit()
967         self.assertEquals(search(['plop'], issue), {i1: {}})
968         self.assertEquals(search(['flebble'], issue), {i1: {}, i2: {}})
970         # change i1's title
971         issue.set(i1, title="plop")
972         self.db.commit()
973         self.assertEquals(search(['plop'], issue), {i1: {}})
974         self.assertEquals(search(['flebble'], issue), {i2: {}})
976     def testReindexingClear(self):
977         search = self.db.indexer.search
978         issue = self.db.issue
979         i1 = issue.create(title="flebble plop")
980         i2 = issue.create(title="flebble frooz")
981         self.db.commit()
982         self.assertEquals(search(['plop'], issue), {i1: {}})
983         self.assertEquals(search(['flebble'], issue), {i1: {}, i2: {}})
985         # unset i1's title
986         issue.set(i1, title="")
987         self.db.commit()
988         self.assertEquals(search(['plop'], issue), {})
989         self.assertEquals(search(['flebble'], issue), {i2: {}})
991     def testFileClassReindexing(self):
992         f1 = self.db.file.create(content='hello')
993         f2 = self.db.file.create(content='hello, world')
994         i1 = self.db.issue.create(files=[f1, f2])
995         self.db.commit()
996         d = self.db.indexer.search(['hello'], self.db.issue)
997         self.assert_(d.has_key(i1))
998         d[i1]['files'].sort()
999         self.assertEquals(d, {i1: {'files': [f1, f2]}})
1000         self.assertEquals(self.db.indexer.search(['world'], self.db.issue),
1001             {i1: {'files': [f2]}})
1002         self.db.file.set(f1, content="world")
1003         self.db.commit()
1004         d = self.db.indexer.search(['world'], self.db.issue)
1005         d[i1]['files'].sort()
1006         self.assertEquals(d, {i1: {'files': [f1, f2]}})
1007         self.assertEquals(self.db.indexer.search(['hello'], self.db.issue),
1008             {i1: {'files': [f2]}})
1010     def testFileClassIndexingNoNoNo(self):
1011         f1 = self.db.file.create(content='hello')
1012         self.db.commit()
1013         self.assertEquals(self.db.indexer.search(['hello'], self.db.file),
1014             {'1': {}})
1016         f1 = self.db.file_nidx.create(content='hello')
1017         self.db.commit()
1018         self.assertEquals(self.db.indexer.search(['hello'], self.db.file_nidx),
1019             {})
1021     def testForcedReindexing(self):
1022         self.db.issue.create(title="flebble frooz")
1023         self.db.commit()
1024         self.assertEquals(self.db.indexer.search(['flebble'], self.db.issue),
1025             {'1': {}})
1026         self.db.indexer.quiet = 1
1027         self.db.indexer.force_reindex()
1028         self.db.post_init()
1029         self.db.indexer.quiet = 9
1030         self.assertEquals(self.db.indexer.search(['flebble'], self.db.issue),
1031             {'1': {}})
1033     def testIndexingPropertiesOnImport(self):
1034         # import an issue
1035         title = 'Bzzt'
1036         nodeid = self.db.issue.import_list(['title', 'messages', 'files',
1037             'spam', 'nosy', 'superseder'], [repr(title), '[]', '[]',
1038             '[]', '[]', '[]'])
1039         self.db.commit()
1041         # Content of title attribute is indexed
1042         self.assertEquals(self.db.indexer.search([title], self.db.issue),
1043             {str(nodeid):{}})
1046     #
1047     # searching tests follow
1048     #
1049     def testFindIncorrectProperty(self):
1050         self.assertRaises(TypeError, self.db.issue.find, title='fubar')
1052     def _find_test_setup(self):
1053         self.db.file.create(content='')
1054         self.db.file.create(content='')
1055         self.db.user.create(username='')
1056         one = self.db.issue.create(status="1", nosy=['1'])
1057         two = self.db.issue.create(status="2", nosy=['2'], files=['1'],
1058             assignedto='2')
1059         three = self.db.issue.create(status="1", nosy=['1','2'])
1060         four = self.db.issue.create(status="3", assignedto='1',
1061             files=['1','2'])
1062         return one, two, three, four
1064     def testFindLink(self):
1065         one, two, three, four = self._find_test_setup()
1066         got = self.db.issue.find(status='1')
1067         got.sort()
1068         self.assertEqual(got, [one, three])
1069         got = self.db.issue.find(status={'1':1})
1070         got.sort()
1071         self.assertEqual(got, [one, three])
1073     def testFindLinkFail(self):
1074         self._find_test_setup()
1075         self.assertEqual(self.db.issue.find(status='4'), [])
1076         self.assertEqual(self.db.issue.find(status={'4':1}), [])
1078     def testFindLinkUnset(self):
1079         one, two, three, four = self._find_test_setup()
1080         got = self.db.issue.find(assignedto=None)
1081         got.sort()
1082         self.assertEqual(got, [one, three])
1083         got = self.db.issue.find(assignedto={None:1})
1084         got.sort()
1085         self.assertEqual(got, [one, three])
1087     def testFindMultipleLink(self):
1088         one, two, three, four = self._find_test_setup()
1089         l = self.db.issue.find(status={'1':1, '3':1})
1090         l.sort()
1091         self.assertEqual(l, [one, three, four])
1092         l = self.db.issue.find(assignedto={None:1, '1':1})
1093         l.sort()
1094         self.assertEqual(l, [one, three, four])
1096     def testFindMultilink(self):
1097         one, two, three, four = self._find_test_setup()
1098         got = self.db.issue.find(nosy='2')
1099         got.sort()
1100         self.assertEqual(got, [two, three])
1101         got = self.db.issue.find(nosy={'2':1})
1102         got.sort()
1103         self.assertEqual(got, [two, three])
1104         got = self.db.issue.find(nosy={'2':1}, files={})
1105         got.sort()
1106         self.assertEqual(got, [two, three])
1108     def testFindMultiMultilink(self):
1109         one, two, three, four = self._find_test_setup()
1110         got = self.db.issue.find(nosy='2', files='1')
1111         got.sort()
1112         self.assertEqual(got, [two, three, four])
1113         got = self.db.issue.find(nosy={'2':1}, files={'1':1})
1114         got.sort()
1115         self.assertEqual(got, [two, three, four])
1117     def testFindMultilinkFail(self):
1118         self._find_test_setup()
1119         self.assertEqual(self.db.issue.find(nosy='3'), [])
1120         self.assertEqual(self.db.issue.find(nosy={'3':1}), [])
1122     def testFindMultilinkUnset(self):
1123         self._find_test_setup()
1124         self.assertEqual(self.db.issue.find(nosy={}), [])
1126     def testFindLinkAndMultilink(self):
1127         one, two, three, four = self._find_test_setup()
1128         got = self.db.issue.find(status='1', nosy='2')
1129         got.sort()
1130         self.assertEqual(got, [one, two, three])
1131         got = self.db.issue.find(status={'1':1}, nosy={'2':1})
1132         got.sort()
1133         self.assertEqual(got, [one, two, three])
1135     def testFindRetired(self):
1136         one, two, three, four = self._find_test_setup()
1137         self.assertEqual(len(self.db.issue.find(status='1')), 2)
1138         self.db.issue.retire(one)
1139         self.assertEqual(len(self.db.issue.find(status='1')), 1)
1141     def testStringFind(self):
1142         self.assertRaises(TypeError, self.db.issue.stringFind, status='1')
1144         ids = []
1145         ids.append(self.db.issue.create(title="spam"))
1146         self.db.issue.create(title="not spam")
1147         ids.append(self.db.issue.create(title="spam"))
1148         ids.sort()
1149         got = self.db.issue.stringFind(title='spam')
1150         got.sort()
1151         self.assertEqual(got, ids)
1152         self.assertEqual(self.db.issue.stringFind(title='fubar'), [])
1154         # test retiring a node
1155         self.db.issue.retire(ids[0])
1156         self.assertEqual(len(self.db.issue.stringFind(title='spam')), 1)
1158     def filteringSetup(self, classname='issue'):
1159         for user in (
1160                 {'username': 'bleep', 'age': 1, 'assignable': True},
1161                 {'username': 'blop', 'age': 1.5, 'assignable': True},
1162                 {'username': 'blorp', 'age': 2, 'assignable': False}):
1163             self.db.user.create(**user)
1164         file_content = ''.join([chr(i) for i in range(255)])
1165         f = self.db.file.create(content=file_content)
1166         for issue in (
1167                 {'title': 'issue one', 'status': '2', 'assignedto': '1',
1168                     'foo': date.Interval('1:10'), 'priority': '3',
1169                     'deadline': date.Date('2003-02-16.22:50')},
1170                 {'title': 'issue two', 'status': '1', 'assignedto': '2',
1171                     'foo': date.Interval('1d'), 'priority': '3',
1172                     'deadline': date.Date('2003-01-01.00:00')},
1173                 {'title': 'issue three', 'status': '1', 'priority': '2',
1174                     'nosy': ['1','2'], 'deadline': date.Date('2003-02-18')},
1175                 {'title': 'non four', 'status': '3',
1176                     'foo': date.Interval('0:10'), 'priority': '2',
1177                     'nosy': ['1','2','3'], 'deadline': date.Date('2004-03-08'),
1178                     'files': [f]}):
1179             self.db.issue.create(**issue)
1180         self.db.commit()
1181         return self.iterSetup(classname)
1183     def testFilteringID(self):
1184         ae, filter, filter_iter = self.filteringSetup()
1185         for filt in filter, filter_iter:
1186             ae(filt(None, {'id': '1'}, ('+','id'), (None,None)), ['1'])
1187             ae(filt(None, {'id': '2'}, ('+','id'), (None,None)), ['2'])
1188             ae(filt(None, {'id': '100'}, ('+','id'), (None,None)), [])
1190     def testFilteringBoolean(self):
1191         ae, filter, filter_iter = self.filteringSetup('user')
1192         a = 'assignable'
1193         for filt in filter, filter_iter:
1194             ae(filt(None, {a: '1'}, ('+','id'), (None,None)), ['3','4'])
1195             ae(filt(None, {a: '0'}, ('+','id'), (None,None)), ['5'])
1196             ae(filt(None, {a: ['1']}, ('+','id'), (None,None)), ['3','4'])
1197             ae(filt(None, {a: ['0']}, ('+','id'), (None,None)), ['5'])
1198             ae(filt(None, {a: ['0','1']}, ('+','id'), (None,None)),
1199                 ['3','4','5'])
1200             ae(filt(None, {a: 'True'}, ('+','id'), (None,None)), ['3','4'])
1201             ae(filt(None, {a: 'False'}, ('+','id'), (None,None)), ['5'])
1202             ae(filt(None, {a: ['True']}, ('+','id'), (None,None)), ['3','4'])
1203             ae(filt(None, {a: ['False']}, ('+','id'), (None,None)), ['5'])
1204             ae(filt(None, {a: ['False','True']}, ('+','id'), (None,None)),
1205                 ['3','4','5'])
1206             ae(filt(None, {a: True}, ('+','id'), (None,None)), ['3','4'])
1207             ae(filt(None, {a: False}, ('+','id'), (None,None)), ['5'])
1208             ae(filt(None, {a: 1}, ('+','id'), (None,None)), ['3','4'])
1209             ae(filt(None, {a: 0}, ('+','id'), (None,None)), ['5'])
1210             ae(filt(None, {a: [1]}, ('+','id'), (None,None)), ['3','4'])
1211             ae(filt(None, {a: [0]}, ('+','id'), (None,None)), ['5'])
1212             ae(filt(None, {a: [0,1]}, ('+','id'), (None,None)), ['3','4','5'])
1213             ae(filt(None, {a: [True]}, ('+','id'), (None,None)), ['3','4'])
1214             ae(filt(None, {a: [False]}, ('+','id'), (None,None)), ['5'])
1215             ae(filt(None, {a: [False,True]}, ('+','id'), (None,None)),
1216                 ['3','4','5'])
1218     def testFilteringNumber(self):
1219         ae, filter, filter_iter = self.filteringSetup('user')
1220         for filt in filter, filter_iter:
1221             ae(filt(None, {'age': '1'}, ('+','id'), (None,None)), ['3'])
1222             ae(filt(None, {'age': '1.5'}, ('+','id'), (None,None)), ['4'])
1223             ae(filt(None, {'age': '2'}, ('+','id'), (None,None)), ['5'])
1224             ae(filt(None, {'age': ['1','2']}, ('+','id'), (None,None)),
1225                 ['3','5'])
1226             ae(filt(None, {'age': 2}, ('+','id'), (None,None)), ['5'])
1227             ae(filt(None, {'age': [1,2]}, ('+','id'), (None,None)), ['3','5'])
1229     def testFilteringString(self):
1230         ae, filter, filter_iter = self.filteringSetup()
1231         for filt in filter, filter_iter:
1232             ae(filt(None, {'title': ['one']}, ('+','id'), (None,None)), ['1'])
1233             ae(filt(None, {'title': ['issue one']}, ('+','id'), (None,None)),
1234                 ['1'])
1235             ae(filt(None, {'title': ['issue', 'one']}, ('+','id'), (None,None)),
1236                 ['1'])
1237             ae(filt(None, {'title': ['issue']}, ('+','id'), (None,None)),
1238                 ['1','2','3'])
1239             ae(filt(None, {'title': ['one', 'two']}, ('+','id'), (None,None)),
1240                 [])
1242     def testFilteringLink(self):
1243         ae, filter, filter_iter = self.filteringSetup()
1244         a = 'assignedto'
1245         grp = (None, None)
1246         for filt in filter, filter_iter:
1247             ae(filt(None, {'status': '1'}, ('+','id'), grp), ['2','3'])
1248             ae(filt(None, {a: '-1'}, ('+','id'), grp), ['3','4'])
1249             ae(filt(None, {a: None}, ('+','id'), grp), ['3','4'])
1250             ae(filt(None, {a: [None]}, ('+','id'), grp), ['3','4'])
1251             ae(filt(None, {a: ['-1', None]}, ('+','id'), grp), ['3','4'])
1252             ae(filt(None, {a: ['1', None]}, ('+','id'), grp), ['1', '3','4'])
1254     def testFilteringMultilinkAndGroup(self):
1255         """testFilteringMultilinkAndGroup:
1256         See roundup Bug 1541128: apparently grouping by something and
1257         searching a Multilink failed with MySQL 5.0
1258         """
1259         ae, filter, filter_iter = self.filteringSetup()
1260         for f in filter, filter_iter:
1261             ae(f(None, {'files': '1'}, ('-','activity'), ('+','status')), ['4'])
1263     def testFilteringRetired(self):
1264         ae, filter, filter_iter = self.filteringSetup()
1265         self.db.issue.retire('2')
1266         for f in filter, filter_iter:
1267             ae(f(None, {'status': '1'}, ('+','id'), (None,None)), ['3'])
1269     def testFilteringMultilink(self):
1270         ae, filter, filter_iter = self.filteringSetup()
1271         for filt in filter, filter_iter:
1272             ae(filt(None, {'nosy': '3'}, ('+','id'), (None,None)), ['4'])
1273             ae(filt(None, {'nosy': '-1'}, ('+','id'), (None,None)), ['1', '2'])
1274             ae(filt(None, {'nosy': ['1','2']}, ('+', 'status'),
1275                 ('-', 'deadline')), ['4', '3'])
1277     def testFilteringMany(self):
1278         ae, filter, filter_iter = self.filteringSetup()
1279         for f in filter, filter_iter:
1280             ae(f(None, {'nosy': '2', 'status': '1'}, ('+','id'), (None,None)),
1281                 ['3'])
1283     def testFilteringRangeBasic(self):
1284         ae, filter, filter_iter = self.filteringSetup()
1285         d = 'deadline'
1286         for f in filter, filter_iter:
1287             ae(f(None, {d: 'from 2003-02-10 to 2003-02-23'}), ['1','3'])
1288             ae(f(None, {d: '2003-02-10; 2003-02-23'}), ['1','3'])
1289             ae(f(None, {d: '; 2003-02-16'}), ['2'])
1291     def testFilteringRangeTwoSyntaxes(self):
1292         ae, filter, filter_iter = self.filteringSetup()
1293         for filt in filter, filter_iter:
1294             ae(filt(None, {'deadline': 'from 2003-02-16'}), ['1', '3', '4'])
1295             ae(filt(None, {'deadline': '2003-02-16;'}), ['1', '3', '4'])
1297     def testFilteringRangeYearMonthDay(self):
1298         ae, filter, filter_iter = self.filteringSetup()
1299         for filt in filter, filter_iter:
1300             ae(filt(None, {'deadline': '2002'}), [])
1301             ae(filt(None, {'deadline': '2003'}), ['1', '2', '3'])
1302             ae(filt(None, {'deadline': '2004'}), ['4'])
1303             ae(filt(None, {'deadline': '2003-02-16'}), ['1'])
1304             ae(filt(None, {'deadline': '2003-02-17'}), [])
1306     def testFilteringRangeMonths(self):
1307         ae, filter, filter_iter = self.filteringSetup()
1308         for month in range(1, 13):
1309             for n in range(1, month+1):
1310                 i = self.db.issue.create(title='%d.%d'%(month, n),
1311                     deadline=date.Date('2001-%02d-%02d.00:00'%(month, n)))
1312         self.db.commit()
1314         for month in range(1, 13):
1315             for filt in filter, filter_iter:
1316                 r = filt(None, dict(deadline='2001-%02d'%month))
1317                 assert len(r) == month, 'month %d != length %d'%(month, len(r))
1319     def testFilteringRangeInterval(self):
1320         ae, filter, filter_iter = self.filteringSetup()
1321         for filt in filter, filter_iter:
1322             ae(filt(None, {'foo': 'from 0:50 to 2:00'}), ['1'])
1323             ae(filt(None, {'foo': 'from 0:50 to 1d 2:00'}), ['1', '2'])
1324             ae(filt(None, {'foo': 'from 5:50'}), ['2'])
1325             ae(filt(None, {'foo': 'to 0:05'}), [])
1327     def testFilteringRangeGeekInterval(self):
1328         ae, filter, filter_iter = self.filteringSetup()
1329         for issue in (
1330                 { 'deadline': date.Date('. -2d')},
1331                 { 'deadline': date.Date('. -1d')},
1332                 { 'deadline': date.Date('. -8d')},
1333                 ):
1334             self.db.issue.create(**issue)
1335         for filt in filter, filter_iter:
1336             ae(filt(None, {'deadline': '-2d;'}), ['5', '6'])
1337             ae(filt(None, {'deadline': '-1d;'}), ['6'])
1338             ae(filt(None, {'deadline': '-1w;'}), ['5', '6'])
1340     def testFilteringIntervalSort(self):
1341         # 1: '1:10'
1342         # 2: '1d'
1343         # 3: None
1344         # 4: '0:10'
1345         ae, filter, filter_iter = self.filteringSetup()
1346         for filt in filter, filter_iter:
1347             # ascending should sort None, 1:10, 1d
1348             ae(filt(None, {}, ('+','foo'), (None,None)), ['3', '4', '1', '2'])
1349             # descending should sort 1d, 1:10, None
1350             ae(filt(None, {}, ('-','foo'), (None,None)), ['2', '1', '4', '3'])
1352     def testFilteringStringSort(self):
1353         # 1: 'issue one'
1354         # 2: 'issue two'
1355         # 3: 'issue three'
1356         # 4: 'non four'
1357         ae, filter, filter_iter = self.filteringSetup()
1358         for filt in filter, filter_iter:
1359             ae(filt(None, {}, ('+','title')), ['1', '3', '2', '4'])
1360             ae(filt(None, {}, ('-','title')), ['4', '2', '3', '1'])
1361         # Test string case: For now allow both, w/wo case matching.
1362         # 1: 'issue one'
1363         # 2: 'issue two'
1364         # 3: 'Issue three'
1365         # 4: 'non four'
1366         self.db.issue.set('3', title='Issue three')
1367         for filt in filter, filter_iter:
1368             ae(filt(None, {}, ('+','title')), ['1', '3', '2', '4'])
1369             ae(filt(None, {}, ('-','title')), ['4', '2', '3', '1'])
1370         # Obscure bug in anydbm backend trying to convert to number
1371         # 1: '1st issue'
1372         # 2: '2'
1373         # 3: 'Issue three'
1374         # 4: 'non four'
1375         self.db.issue.set('1', title='1st issue')
1376         self.db.issue.set('2', title='2')
1377         for filt in filter, filter_iter:
1378             ae(filt(None, {}, ('+','title')), ['1', '2', '3', '4'])
1379             ae(filt(None, {}, ('-','title')), ['4', '3', '2', '1'])
1381     def testFilteringMultilinkSort(self):
1382         # 1: []                 Reverse:  1: []
1383         # 2: []                           2: []
1384         # 3: ['admin','fred']             3: ['fred','admin']
1385         # 4: ['admin','bleep','fred']     4: ['fred','bleep','admin']
1386         # Note the sort order for the multilink doen't change when
1387         # reversing the sort direction due to the re-sorting of the
1388         # multilink!
1389         # Note that we don't test filter_iter here, Multilink sort-order
1390         # isn't defined for that.
1391         ae, filt, dummy = self.filteringSetup()
1392         ae(filt(None, {}, ('+','nosy'), (None,None)), ['1', '2', '4', '3'])
1393         ae(filt(None, {}, ('-','nosy'), (None,None)), ['4', '3', '1', '2'])
1395     def testFilteringMultilinkSortGroup(self):
1396         # 1: status: 2 "in-progress" nosy: []
1397         # 2: status: 1 "unread"      nosy: []
1398         # 3: status: 1 "unread"      nosy: ['admin','fred']
1399         # 4: status: 3 "testing"     nosy: ['admin','bleep','fred']
1400         # Note that we don't test filter_iter here, Multilink sort-order
1401         # isn't defined for that.
1402         ae, filt, dummy = self.filteringSetup()
1403         ae(filt(None, {}, ('+','nosy'), ('+','status')), ['1', '4', '2', '3'])
1404         ae(filt(None, {}, ('-','nosy'), ('+','status')), ['1', '4', '3', '2'])
1405         ae(filt(None, {}, ('+','nosy'), ('-','status')), ['2', '3', '4', '1'])
1406         ae(filt(None, {}, ('-','nosy'), ('-','status')), ['3', '2', '4', '1'])
1407         ae(filt(None, {}, ('+','status'), ('+','nosy')), ['1', '2', '4', '3'])
1408         ae(filt(None, {}, ('-','status'), ('+','nosy')), ['2', '1', '4', '3'])
1409         ae(filt(None, {}, ('+','status'), ('-','nosy')), ['4', '3', '1', '2'])
1410         ae(filt(None, {}, ('-','status'), ('-','nosy')), ['4', '3', '2', '1'])
1412     def testFilteringLinkSortGroup(self):
1413         # 1: status: 2 -> 'i', priority: 3 -> 1
1414         # 2: status: 1 -> 'u', priority: 3 -> 1
1415         # 3: status: 1 -> 'u', priority: 2 -> 3
1416         # 4: status: 3 -> 't', priority: 2 -> 3
1417         ae, filter, filter_iter = self.filteringSetup()
1418         for filt in filter, filter_iter:
1419             ae(filt(None, {}, ('+','status'), ('+','priority')),
1420                 ['1', '2', '4', '3'])
1421             ae(filt(None, {'priority':'2'}, ('+','status'), ('+','priority')),
1422                 ['4', '3'])
1423             ae(filt(None, {'priority.order':'3'}, ('+','status'),
1424                 ('+','priority')), ['4', '3'])
1425             ae(filt(None, {'priority':['2','3']}, ('+','priority'),
1426                 ('+','status')), ['1', '4', '2', '3'])
1427             ae(filt(None, {}, ('+','priority'), ('+','status')),
1428                 ['1', '4', '2', '3'])
1430     def testFilteringDateSort(self):
1431         # '1': '2003-02-16.22:50'
1432         # '2': '2003-01-01.00:00'
1433         # '3': '2003-02-18'
1434         # '4': '2004-03-08'
1435         ae, filter, filter_iter = self.filteringSetup()
1436         for f in filter, filter_iter:
1437             # ascending
1438             ae(f(None, {}, ('+','deadline'), (None,None)), ['2', '1', '3', '4'])
1439             # descending
1440             ae(f(None, {}, ('-','deadline'), (None,None)), ['4', '3', '1', '2'])
1442     def testFilteringDateSortPriorityGroup(self):
1443         # '1': '2003-02-16.22:50'  1 => 2
1444         # '2': '2003-01-01.00:00'  3 => 1
1445         # '3': '2003-02-18'        2 => 3
1446         # '4': '2004-03-08'        1 => 2
1447         ae, filter, filter_iter = self.filteringSetup()
1449         for filt in filter, filter_iter:
1450             # ascending
1451             ae(filt(None, {}, ('+','deadline'), ('+','priority')),
1452                 ['2', '1', '3', '4'])
1453             ae(filt(None, {}, ('-','deadline'), ('+','priority')),
1454                 ['1', '2', '4', '3'])
1455             # descending
1456             ae(filt(None, {}, ('+','deadline'), ('-','priority')),
1457                 ['3', '4', '2', '1'])
1458             ae(filt(None, {}, ('-','deadline'), ('-','priority')),
1459                 ['4', '3', '1', '2'])
1461     def testFilteringTransitiveLinkUser(self):
1462         ae, filter, filter_iter = self.filteringSetupTransitiveSearch('user')
1463         for f in filter, filter_iter:
1464             ae(f(None, {'supervisor.username': 'ceo'}, ('+','username')),
1465                 ['4', '5'])
1466             ae(f(None, {'supervisor.supervisor.username': 'ceo'},
1467                 ('+','username')), ['6', '7', '8', '9', '10'])
1468             ae(f(None, {'supervisor.supervisor': '3'}, ('+','username')),
1469                 ['6', '7', '8', '9', '10'])
1470             ae(f(None, {'supervisor.supervisor.id': '3'}, ('+','username')),
1471                 ['6', '7', '8', '9', '10'])
1472             ae(f(None, {'supervisor.username': 'grouplead1'}, ('+','username')),
1473                 ['6', '7'])
1474             ae(f(None, {'supervisor.username': 'grouplead2'}, ('+','username')),
1475                 ['8', '9', '10'])
1476             ae(f(None, {'supervisor.username': 'grouplead2',
1477                 'supervisor.supervisor.username': 'ceo'}, ('+','username')),
1478                 ['8', '9', '10'])
1479             ae(f(None, {'supervisor.supervisor': '3', 'supervisor': '4'},
1480                 ('+','username')), ['6', '7'])
1482     def testFilteringTransitiveLinkSort(self):
1483         ae, filter, filter_iter = self.filteringSetupTransitiveSearch()
1484         ae, ufilter, ufilter_iter = self.iterSetup('user')
1485         # Need to make ceo his own (and first two users') supervisor,
1486         # otherwise we will depend on sorting order of NULL values.
1487         # Leave that to a separate test.
1488         self.db.user.set('1', supervisor = '3')
1489         self.db.user.set('2', supervisor = '3')
1490         self.db.user.set('3', supervisor = '3')
1491         for ufilt in ufilter, ufilter_iter:
1492             ae(ufilt(None, {'supervisor':'3'}, []), ['1', '2', '3', '4', '5'])
1493             ae(ufilt(None, {}, [('+','supervisor.supervisor.supervisor'),
1494                 ('+','supervisor.supervisor'), ('+','supervisor'),
1495                 ('+','username')]),
1496                 ['1', '3', '2', '4', '5', '6', '7', '8', '9', '10'])
1497             ae(ufilt(None, {}, [('+','supervisor.supervisor.supervisor'),
1498                 ('-','supervisor.supervisor'), ('-','supervisor'),
1499                 ('+','username')]),
1500                 ['8', '9', '10', '6', '7', '1', '3', '2', '4', '5'])
1501         for f in filter, filter_iter:
1502             ae(f(None, {}, [('+','assignedto.supervisor.supervisor.supervisor'),
1503                 ('+','assignedto.supervisor.supervisor'),
1504                 ('+','assignedto.supervisor'), ('+','assignedto')]),
1505                 ['1', '2', '3', '4', '5', '6', '7', '8'])
1506             ae(f(None, {}, [('+','assignedto.supervisor.supervisor.supervisor'),
1507                 ('+','assignedto.supervisor.supervisor'),
1508                 ('-','assignedto.supervisor'), ('+','assignedto')]),
1509                 ['4', '5', '6', '7', '8', '1', '2', '3'])
1510             ae(f(None, {}, [('+','assignedto.supervisor.supervisor.supervisor'),
1511                 ('+','assignedto.supervisor.supervisor'),
1512                 ('+','assignedto.supervisor'), ('+','assignedto'),
1513                 ('-','status')]),
1514                 ['2', '1', '3', '4', '5', '6', '8', '7'])
1515             ae(f(None, {}, [('+','assignedto.supervisor.supervisor.supervisor'),
1516                 ('+','assignedto.supervisor.supervisor'),
1517                 ('+','assignedto.supervisor'), ('+','assignedto'),
1518                 ('+','status')]),
1519                 ['1', '2', '3', '4', '5', '7', '6', '8'])
1520             ae(f(None, {}, [('+','assignedto.supervisor.supervisor.supervisor'),
1521                 ('+','assignedto.supervisor.supervisor'),
1522                 ('-','assignedto.supervisor'), ('+','assignedto'),
1523                 ('+','status')]), ['4', '5', '7', '6', '8', '1', '2', '3'])
1524             ae(f(None, {'assignedto':['6','7','8','9','10']},
1525                 [('+','assignedto.supervisor.supervisor.supervisor'),
1526                 ('+','assignedto.supervisor.supervisor'),
1527                 ('-','assignedto.supervisor'), ('+','assignedto'),
1528                 ('+','status')]), ['4', '5', '7', '6', '8', '1', '2', '3'])
1529             ae(f(None, {'assignedto':['6','7','8','9']},
1530                 [('+','assignedto.supervisor.supervisor.supervisor'),
1531                 ('+','assignedto.supervisor.supervisor'),
1532                 ('-','assignedto.supervisor'), ('+','assignedto'),
1533                 ('+','status')]), ['4', '5', '1', '2', '3'])
1535     def testFilteringTransitiveLinkSortNull(self):
1536         """Check sorting of NULL values"""
1537         ae, filter, filter_iter = self.filteringSetupTransitiveSearch()
1538         ae, ufilter, ufilter_iter = self.iterSetup('user')
1539         for ufilt in ufilter, ufilter_iter:
1540             ae(ufilt(None, {}, [('+','supervisor.supervisor.supervisor'),
1541                 ('+','supervisor.supervisor'), ('+','supervisor'),
1542                 ('+','username')]),
1543                 ['1', '3', '2', '4', '5', '6', '7', '8', '9', '10'])
1544             ae(ufilt(None, {}, [('+','supervisor.supervisor.supervisor'),
1545                 ('-','supervisor.supervisor'), ('-','supervisor'),
1546                 ('+','username')]),
1547                 ['8', '9', '10', '6', '7', '4', '5', '1', '3', '2'])
1548         for f in filter, filter_iter:
1549             ae(f(None, {}, [('+','assignedto.supervisor.supervisor.supervisor'),
1550                 ('+','assignedto.supervisor.supervisor'),
1551                 ('+','assignedto.supervisor'), ('+','assignedto')]),
1552                 ['1', '2', '3', '4', '5', '6', '7', '8'])
1553             ae(f(None, {}, [('+','assignedto.supervisor.supervisor.supervisor'),
1554                 ('+','assignedto.supervisor.supervisor'),
1555                 ('-','assignedto.supervisor'), ('+','assignedto')]),
1556                 ['4', '5', '6', '7', '8', '1', '2', '3'])
1558     def testFilteringTransitiveLinkIssue(self):
1559         ae, filter, filter_iter = self.filteringSetupTransitiveSearch()
1560         for filt in filter, filter_iter:
1561             ae(filt(None, {'assignedto.supervisor.username': 'grouplead1'},
1562                 ('+','id')), ['1', '2', '3'])
1563             ae(filt(None, {'assignedto.supervisor.username': 'grouplead2'},
1564                 ('+','id')), ['4', '5', '6', '7', '8'])
1565             ae(filt(None, {'assignedto.supervisor.username': 'grouplead2',
1566                            'status': '1'}, ('+','id')), ['4', '6', '8'])
1567             ae(filt(None, {'assignedto.supervisor.username': 'grouplead2',
1568                            'status': '2'}, ('+','id')), ['5', '7'])
1569             ae(filt(None, {'assignedto.supervisor.username': ['grouplead2'],
1570                            'status': '2'}, ('+','id')), ['5', '7'])
1571             ae(filt(None, {'assignedto.supervisor': ['4', '5'], 'status': '2'},
1572                 ('+','id')), ['1', '3', '5', '7'])
1574     def testFilteringTransitiveMultilink(self):
1575         ae, filter, filter_iter = self.filteringSetupTransitiveSearch()
1576         for filt in filter, filter_iter:
1577             ae(filt(None, {'messages.author.username': 'grouplead1'},
1578                 ('+','id')), [])
1579             ae(filt(None, {'messages.author': '6'},
1580                 ('+','id')), ['1', '2'])
1581             ae(filt(None, {'messages.author.id': '6'},
1582                 ('+','id')), ['1', '2'])
1583             ae(filt(None, {'messages.author.username': 'worker1'},
1584                 ('+','id')), ['1', '2'])
1585             ae(filt(None, {'messages.author': '10'},
1586                 ('+','id')), ['6', '7', '8'])
1587             ae(filt(None, {'messages.author': '9'},
1588                 ('+','id')), ['5', '8'])
1589             ae(filt(None, {'messages.author': ['9', '10']},
1590                 ('+','id')), ['5', '6', '7', '8'])
1591             ae(filt(None, {'messages.author': ['8', '9']},
1592                 ('+','id')), ['4', '5', '8'])
1593             ae(filt(None, {'messages.author': ['8', '9'], 'status' : '1'},
1594                 ('+','id')), ['4', '8'])
1595             ae(filt(None, {'messages.author': ['8', '9'], 'status' : '2'},
1596                 ('+','id')), ['5'])
1597             ae(filt(None, {'messages.author': ['8', '9', '10'],
1598                 'messages.date': '2006-01-22.21:00;2006-01-23'}, ('+','id')),
1599                 ['6', '7', '8'])
1600             ae(filt(None, {'nosy.supervisor.username': 'ceo'},
1601                 ('+','id')), ['1', '2'])
1602             ae(filt(None, {'messages.author': ['6', '9']},
1603                 ('+','id')), ['1', '2', '5', '8'])
1604             ae(filt(None, {'messages': ['5', '7']},
1605                 ('+','id')), ['3', '5', '8'])
1606             ae(filt(None, {'messages.author': ['6', '9'],
1607                 'messages': ['5', '7']}, ('+','id')), ['5', '8'])
1609     def testFilteringTransitiveMultilinkSort(self):
1610         # Note that we don't test filter_iter here, Multilink sort-order
1611         # isn't defined for that.
1612         ae, filt, dummy = self.filteringSetupTransitiveSearch()
1613         ae(filt(None, {}, [('+','messages.author')]),
1614             ['1', '2', '3', '4', '5', '8', '6', '7'])
1615         ae(filt(None, {}, [('-','messages.author')]),
1616             ['8', '6', '7', '5', '4', '3', '1', '2'])
1617         ae(filt(None, {}, [('+','messages.date')]),
1618             ['6', '7', '8', '5', '4', '3', '1', '2'])
1619         ae(filt(None, {}, [('-','messages.date')]),
1620             ['1', '2', '3', '4', '8', '5', '6', '7'])
1621         ae(filt(None, {}, [('+','messages.author'),('+','messages.date')]),
1622             ['1', '2', '3', '4', '5', '8', '6', '7'])
1623         ae(filt(None, {}, [('-','messages.author'),('+','messages.date')]),
1624             ['8', '6', '7', '5', '4', '3', '1', '2'])
1625         ae(filt(None, {}, [('+','messages.author'),('-','messages.date')]),
1626             ['1', '2', '3', '4', '5', '8', '6', '7'])
1627         ae(filt(None, {}, [('-','messages.author'),('-','messages.date')]),
1628             ['8', '6', '7', '5', '4', '3', '1', '2'])
1629         ae(filt(None, {}, [('+','messages.author'),('+','assignedto')]),
1630             ['1', '2', '3', '4', '5', '8', '6', '7'])
1631         ae(filt(None, {}, [('+','messages.author'),
1632             ('-','assignedto.supervisor'),('-','assignedto')]),
1633             ['1', '2', '3', '4', '5', '8', '6', '7'])
1634         ae(filt(None, {},
1635             [('+','messages.author.supervisor.supervisor.supervisor'),
1636             ('+','messages.author.supervisor.supervisor'),
1637             ('+','messages.author.supervisor'), ('+','messages.author')]),
1638             ['1', '2', '3', '4', '5', '6', '7', '8'])
1639         self.db.user.setorderprop('age')
1640         self.db.msg.setorderprop('date')
1641         ae(filt(None, {}, [('+','messages'), ('+','messages.author')]),
1642             ['6', '7', '8', '5', '4', '3', '1', '2'])
1643         ae(filt(None, {}, [('+','messages.author'), ('+','messages')]),
1644             ['6', '7', '8', '5', '4', '3', '1', '2'])
1645         self.db.msg.setorderprop('author')
1646         # Orderprop is a Link/Multilink:
1647         # messages are sorted by orderprop().labelprop(), i.e. by
1648         # author.username, *not* by author.orderprop() (author.age)!
1649         ae(filt(None, {}, [('+','messages')]),
1650             ['1', '2', '3', '4', '5', '8', '6', '7'])
1651         ae(filt(None, {}, [('+','messages.author'), ('+','messages')]),
1652             ['6', '7', '8', '5', '4', '3', '1', '2'])
1653         # The following will sort by
1654         # author.supervisor.username and then by
1655         # author.username
1656         # I've resited the tempation to implement recursive orderprop
1657         # here: There could even be loops if several classes specify a
1658         # Link or Multilink as the orderprop...
1659         # msg: 4: worker1 (id  5) : grouplead1 (id 4) ceo (id 3)
1660         # msg: 5: worker2 (id  7) : grouplead1 (id 4) ceo (id 3)
1661         # msg: 6: worker3 (id  8) : grouplead2 (id 5) ceo (id 3)
1662         # msg: 7: worker4 (id  9) : grouplead2 (id 5) ceo (id 3)
1663         # msg: 8: worker5 (id 10) : grouplead2 (id 5) ceo (id 3)
1664         # issue 1: messages 4   sortkey:[[grouplead1], [worker1], 1]
1665         # issue 2: messages 4   sortkey:[[grouplead1], [worker1], 2]
1666         # issue 3: messages 5   sortkey:[[grouplead1], [worker2], 3]
1667         # issue 4: messages 6   sortkey:[[grouplead2], [worker3], 4]
1668         # issue 5: messages 7   sortkey:[[grouplead2], [worker4], 5]
1669         # issue 6: messages 8   sortkey:[[grouplead2], [worker5], 6]
1670         # issue 7: messages 8   sortkey:[[grouplead2], [worker5], 7]
1671         # issue 8: messages 7,8 sortkey:[[grouplead2, grouplead2], ...]
1672         self.db.user.setorderprop('supervisor')
1673         ae(filt(None, {}, [('+','messages.author'), ('-','messages')]),
1674             ['3', '1', '2', '6', '7', '5', '4', '8'])
1676     def testFilteringSortId(self):
1677         ae, filter, filter_iter = self.filteringSetupTransitiveSearch('user')
1678         for filt in filter, filter_iter:
1679             ae(filt(None, {}, ('+','id')),
1680                 ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'])
1682 # XXX add sorting tests for other types
1684     # nuke and re-create db for restore
1685     def nukeAndCreate(self):
1686         # shut down this db and nuke it
1687         self.db.close()
1688         self.nuke_database()
1690         # open a new, empty database
1691         os.makedirs(config.DATABASE + '/files')
1692         self.db = self.module.Database(config, 'admin')
1693         setupSchema(self.db, 0, self.module)
1695     def testImportExport(self):
1696         # use the filtering setup to create a bunch of items
1697         ae, dummy1, dummy2 = self.filteringSetup()
1698         # Get some stuff into the journal for testing import/export of
1699         # journal data:
1700         self.db.user.set('4', password = password.Password('xyzzy'))
1701         self.db.user.set('4', age = 3)
1702         self.db.user.set('4', assignable = True)
1703         self.db.issue.set('1', title = 'i1', status = '3')
1704         self.db.issue.set('1', deadline = date.Date('2007'))
1705         self.db.issue.set('1', foo = date.Interval('1:20'))
1706         p = self.db.priority.create(name = 'some_prio_without_order')
1707         self.db.commit()
1708         self.db.user.set('4', password = password.Password('123xyzzy'))
1709         self.db.user.set('4', assignable = False)
1710         self.db.priority.set(p, order = '4711')
1711         self.db.commit()
1713         self.db.user.retire('3')
1714         self.db.issue.retire('2')
1716         # grab snapshot of the current database
1717         orig = {}
1718         origj = {}
1719         for cn,klass in self.db.classes.items():
1720             cl = orig[cn] = {}
1721             jn = origj[cn] = {}
1722             for id in klass.list():
1723                 it = cl[id] = {}
1724                 jn[id] = self.db.getjournal(cn, id)
1725                 for name in klass.getprops().keys():
1726                     it[name] = klass.get(id, name)
1728         os.mkdir('_test_export')
1729         try:
1730             # grab the export
1731             export = {}
1732             journals = {}
1733             for cn,klass in self.db.classes.items():
1734                 names = klass.export_propnames()
1735                 cl = export[cn] = [names+['is retired']]
1736                 for id in klass.getnodeids():
1737                     cl.append(klass.export_list(names, id))
1738                     if hasattr(klass, 'export_files'):
1739                         klass.export_files('_test_export', id)
1740                 journals[cn] = klass.export_journals()
1742             self.nukeAndCreate()
1744             # import
1745             for cn, items in export.items():
1746                 klass = self.db.classes[cn]
1747                 names = items[0]
1748                 maxid = 1
1749                 for itemprops in items[1:]:
1750                     id = int(klass.import_list(names, itemprops))
1751                     if hasattr(klass, 'import_files'):
1752                         klass.import_files('_test_export', str(id))
1753                     maxid = max(maxid, id)
1754                 self.db.setid(cn, str(maxid+1))
1755                 klass.import_journals(journals[cn])
1756             # This is needed, otherwise journals won't be there for anydbm
1757             self.db.commit()
1758         finally:
1759             shutil.rmtree('_test_export')
1761         # compare with snapshot of the database
1762         for cn, items in orig.iteritems():
1763             klass = self.db.classes[cn]
1764             propdefs = klass.getprops(1)
1765             # ensure retired items are retired :)
1766             l = items.keys(); l.sort()
1767             m = klass.list(); m.sort()
1768             ae(l, m, '%s id list wrong %r vs. %r'%(cn, l, m))
1769             for id, props in items.items():
1770                 for name, value in props.items():
1771                     l = klass.get(id, name)
1772                     if isinstance(value, type([])):
1773                         value.sort()
1774                         l.sort()
1775                     try:
1776                         ae(l, value)
1777                     except AssertionError:
1778                         if not isinstance(propdefs[name], Date):
1779                             raise
1780                         # don't get hung up on rounding errors
1781                         assert not l.__cmp__(value, int_seconds=1)
1782         for jc, items in origj.iteritems():
1783             for id, oj in items.iteritems():
1784                 rj = self.db.getjournal(jc, id)
1785                 # Both mysql and postgresql have some minor issues with
1786                 # rounded seconds on export/import, so we compare only
1787                 # the integer part.
1788                 for j in oj:
1789                     j[1].second = float(int(j[1].second))
1790                 for j in rj:
1791                     j[1].second = float(int(j[1].second))
1792                 oj.sort()
1793                 rj.sort()
1794                 ae(oj, rj)
1796         # make sure the retired items are actually imported
1797         ae(self.db.user.get('4', 'username'), 'blop')
1798         ae(self.db.issue.get('2', 'title'), 'issue two')
1800         # make sure id counters are set correctly
1801         maxid = max([int(id) for id in self.db.user.list()])
1802         newid = self.db.user.create(username='testing')
1803         assert newid > maxid
1805     # test import/export via admin interface
1806     def testAdminImportExport(self):
1807         import roundup.admin
1808         import csv
1809         # use the filtering setup to create a bunch of items
1810         ae, dummy1, dummy2 = self.filteringSetup()
1811         # create large field
1812         self.db.priority.create(name = 'X' * 500)
1813         self.db.config.CSV_FIELD_SIZE = 400
1814         self.db.commit()
1815         output = []
1816         # ugly hack to get stderr output and disable stdout output
1817         # during regression test. Depends on roundup.admin not using
1818         # anything but stdout/stderr from sys (which is currently the
1819         # case)
1820         def stderrwrite(s):
1821             output.append(s)
1822         roundup.admin.sys = MockNull ()
1823         try:
1824             roundup.admin.sys.stderr.write = stderrwrite
1825             tool = roundup.admin.AdminTool()
1826             home = '.'
1827             tool.tracker_home = home
1828             tool.db = self.db
1829             tool.verbose = False
1830             tool.do_export (['_test_export'])
1831             self.assertEqual(len(output), 2)
1832             self.assertEqual(output [1], '\n')
1833             self.failUnless(output [0].startswith
1834                 ('Warning: config csv_field_size should be at least'))
1835             self.failUnless(int(output[0].split()[-1]) > 500)
1837             if hasattr(roundup.admin.csv, 'field_size_limit'):
1838                 self.nukeAndCreate()
1839                 self.db.config.CSV_FIELD_SIZE = 400
1840                 tool = roundup.admin.AdminTool()
1841                 tool.tracker_home = home
1842                 tool.db = self.db
1843                 tool.verbose = False
1844                 self.assertRaises(csv.Error, tool.do_import, ['_test_export'])
1846             self.nukeAndCreate()
1847             self.db.config.CSV_FIELD_SIZE = 3200
1848             tool = roundup.admin.AdminTool()
1849             tool.tracker_home = home
1850             tool.db = self.db
1851             tool.verbose = False
1852             tool.do_import(['_test_export'])
1853         finally:
1854             roundup.admin.sys = sys
1855             shutil.rmtree('_test_export')
1857     def testAddProperty(self):
1858         self.db.issue.create(title="spam", status='1')
1859         self.db.commit()
1861         self.db.issue.addprop(fixer=Link("user"))
1862         # force any post-init stuff to happen
1863         self.db.post_init()
1864         props = self.db.issue.getprops()
1865         keys = props.keys()
1866         keys.sort()
1867         self.assertEqual(keys, ['activity', 'actor', 'assignedto', 'creation',
1868             'creator', 'deadline', 'feedback', 'files', 'fixer', 'foo', 'id', 'messages',
1869             'nosy', 'priority', 'spam', 'status', 'superseder', 'title'])
1870         self.assertEqual(self.db.issue.get('1', "fixer"), None)
1872     def testRemoveProperty(self):
1873         self.db.issue.create(title="spam", status='1')
1874         self.db.commit()
1876         del self.db.issue.properties['title']
1877         self.db.post_init()
1878         props = self.db.issue.getprops()
1879         keys = props.keys()
1880         keys.sort()
1881         self.assertEqual(keys, ['activity', 'actor', 'assignedto', 'creation',
1882             'creator', 'deadline', 'feedback', 'files', 'foo', 'id', 'messages',
1883             'nosy', 'priority', 'spam', 'status', 'superseder'])
1884         self.assertEqual(self.db.issue.list(), ['1'])
1886     def testAddRemoveProperty(self):
1887         self.db.issue.create(title="spam", status='1')
1888         self.db.commit()
1890         self.db.issue.addprop(fixer=Link("user"))
1891         del self.db.issue.properties['title']
1892         self.db.post_init()
1893         props = self.db.issue.getprops()
1894         keys = props.keys()
1895         keys.sort()
1896         self.assertEqual(keys, ['activity', 'actor', 'assignedto', 'creation',
1897             'creator', 'deadline', 'feedback', 'files', 'fixer', 'foo', 'id',
1898             'messages', 'nosy', 'priority', 'spam', 'status', 'superseder'])
1899         self.assertEqual(self.db.issue.list(), ['1'])
1901     def testNosyMail(self) :
1902         """Creates one issue with two attachments, one smaller and one larger
1903            than the set max_attachment_size.
1904         """
1905         old_translate_ = roundupdb._
1906         roundupdb._ = i18n.get_translation(language='C').gettext
1907         db = self.db
1908         db.config.NOSY_MAX_ATTACHMENT_SIZE = 4096
1909         res = dict(mail_to = None, mail_msg = None)
1910         def dummy_snd(s, to, msg, res=res) :
1911             res["mail_to"], res["mail_msg"] = to, msg
1912         backup, Mailer.smtp_send = Mailer.smtp_send, dummy_snd
1913         try :
1914             f1 = db.file.create(name="test1.txt", content="x" * 20)
1915             f2 = db.file.create(name="test2.txt", content="y" * 5000)
1916             m  = db.msg.create(content="one two", author="admin",
1917                 files = [f1, f2])
1918             i  = db.issue.create(title='spam', files = [f1, f2],
1919                 messages = [m], nosy = [db.user.lookup("fred")])
1921             db.issue.nosymessage(i, m, {})
1922             mail_msg = str(res["mail_msg"])
1923             self.assertEqual(res["mail_to"], ["fred@example.com"])
1924             self.assert_("From: admin" in mail_msg)
1925             self.assert_("Subject: [issue1] spam" in mail_msg)
1926             self.assert_("New submission from admin" in mail_msg)
1927             self.assert_("one two" in mail_msg)
1928             self.assert_("File 'test1.txt' not attached" not in mail_msg)
1929             self.assert_(base64.encodestring("xxx").rstrip() in mail_msg)
1930             self.assert_("File 'test2.txt' not attached" in mail_msg)
1931             self.assert_(base64.encodestring("yyy").rstrip() not in mail_msg)
1932         finally :
1933             roundupdb._ = old_translate_
1934             Mailer.smtp_send = backup
1936 class ROTest(MyTestCase):
1937     def setUp(self):
1938         # remove previous test, ignore errors
1939         if os.path.exists(config.DATABASE):
1940             shutil.rmtree(config.DATABASE)
1941         os.makedirs(config.DATABASE + '/files')
1942         self.db = self.module.Database(config, 'admin')
1943         setupSchema(self.db, 1, self.module)
1944         self.db.close()
1946         self.db = self.module.Database(config)
1947         setupSchema(self.db, 0, self.module)
1949     def testExceptions(self):
1950         # this tests the exceptions that should be raised
1951         ar = self.assertRaises
1953         # this tests the exceptions that should be raised
1954         ar(DatabaseError, self.db.status.create, name="foo")
1955         ar(DatabaseError, self.db.status.set, '1', name="foo")
1956         ar(DatabaseError, self.db.status.retire, '1')
1959 class SchemaTest(MyTestCase):
1960     def setUp(self):
1961         # remove previous test, ignore errors
1962         if os.path.exists(config.DATABASE):
1963             shutil.rmtree(config.DATABASE)
1964         os.makedirs(config.DATABASE + '/files')
1966     def test_reservedProperties(self):
1967         self.open_database()
1968         self.assertRaises(ValueError, self.module.Class, self.db, "a",
1969             creation=String())
1970         self.assertRaises(ValueError, self.module.Class, self.db, "a",
1971             activity=String())
1972         self.assertRaises(ValueError, self.module.Class, self.db, "a",
1973             creator=String())
1974         self.assertRaises(ValueError, self.module.Class, self.db, "a",
1975             actor=String())
1977     def init_a(self):
1978         self.open_database()
1979         a = self.module.Class(self.db, "a", name=String())
1980         a.setkey("name")
1981         self.db.post_init()
1983     def test_fileClassProps(self):
1984         self.open_database()
1985         a = self.module.FileClass(self.db, 'a')
1986         l = a.getprops().keys()
1987         l.sort()
1988         self.assert_(l, ['activity', 'actor', 'content', 'created',
1989             'creation', 'type'])
1991     def init_ab(self):
1992         self.open_database()
1993         a = self.module.Class(self.db, "a", name=String())
1994         a.setkey("name")
1995         b = self.module.Class(self.db, "b", name=String(),
1996             fooz=Multilink('a'))
1997         b.setkey("name")
1998         self.db.post_init()
2000     def test_addNewClass(self):
2001         self.init_a()
2003         self.assertRaises(ValueError, self.module.Class, self.db, "a",
2004             name=String())
2006         aid = self.db.a.create(name='apple')
2007         self.db.commit(); self.db.close()
2009         # add a new class to the schema and check creation of new items
2010         # (and existence of old ones)
2011         self.init_ab()
2012         bid = self.db.b.create(name='bear', fooz=[aid])
2013         self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
2014         self.db.commit()
2015         self.db.close()
2017         # now check we can recall the added class' items
2018         self.init_ab()
2019         self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
2020         self.assertEqual(self.db.a.lookup('apple'), aid)
2021         self.assertEqual(self.db.b.get(bid, 'name'), 'bear')
2022         self.assertEqual(self.db.b.get(bid, 'fooz'), [aid])
2023         self.assertEqual(self.db.b.lookup('bear'), bid)
2025         # confirm journal's ok
2026         self.db.getjournal('a', aid)
2027         self.db.getjournal('b', bid)
2029     def init_amod(self):
2030         self.open_database()
2031         a = self.module.Class(self.db, "a", name=String(), newstr=String(),
2032             newint=Interval(), newnum=Number(), newbool=Boolean(),
2033             newdate=Date())
2034         a.setkey("name")
2035         b = self.module.Class(self.db, "b", name=String())
2036         b.setkey("name")
2037         self.db.post_init()
2039     def test_modifyClass(self):
2040         self.init_ab()
2042         # add item to user and issue class
2043         aid = self.db.a.create(name='apple')
2044         bid = self.db.b.create(name='bear')
2045         self.db.commit(); self.db.close()
2047         # modify "a" schema
2048         self.init_amod()
2049         self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
2050         self.assertEqual(self.db.a.get(aid, 'newstr'), None)
2051         self.assertEqual(self.db.a.get(aid, 'newint'), None)
2052         # hack - metakit can't return None for missing values, and we're not
2053         # really checking for that behavior here anyway
2054         self.assert_(not self.db.a.get(aid, 'newnum'))
2055         self.assert_(not self.db.a.get(aid, 'newbool'))
2056         self.assertEqual(self.db.a.get(aid, 'newdate'), None)
2057         self.assertEqual(self.db.b.get(aid, 'name'), 'bear')
2058         aid2 = self.db.a.create(name='aardvark', newstr='booz')
2059         self.db.commit(); self.db.close()
2061         # test
2062         self.init_amod()
2063         self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
2064         self.assertEqual(self.db.a.get(aid, 'newstr'), None)
2065         self.assertEqual(self.db.b.get(aid, 'name'), 'bear')
2066         self.assertEqual(self.db.a.get(aid2, 'name'), 'aardvark')
2067         self.assertEqual(self.db.a.get(aid2, 'newstr'), 'booz')
2069         # confirm journal's ok
2070         self.db.getjournal('a', aid)
2071         self.db.getjournal('a', aid2)
2073     def init_amodkey(self):
2074         self.open_database()
2075         a = self.module.Class(self.db, "a", name=String(), newstr=String())
2076         a.setkey("newstr")
2077         b = self.module.Class(self.db, "b", name=String())
2078         b.setkey("name")
2079         self.db.post_init()
2081     def test_changeClassKey(self):
2082         self.init_amod()
2083         aid = self.db.a.create(name='apple')
2084         self.assertEqual(self.db.a.lookup('apple'), aid)
2085         self.db.commit(); self.db.close()
2087         # change the key to newstr on a
2088         self.init_amodkey()
2089         self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
2090         self.assertEqual(self.db.a.get(aid, 'newstr'), None)
2091         self.assertRaises(KeyError, self.db.a.lookup, 'apple')
2092         aid2 = self.db.a.create(name='aardvark', newstr='booz')
2093         self.db.commit(); self.db.close()
2095         # check
2096         self.init_amodkey()
2097         self.assertEqual(self.db.a.lookup('booz'), aid2)
2099         # confirm journal's ok
2100         self.db.getjournal('a', aid)
2102     def test_removeClassKey(self):
2103         self.init_amod()
2104         aid = self.db.a.create(name='apple')
2105         self.assertEqual(self.db.a.lookup('apple'), aid)
2106         self.db.commit(); self.db.close()
2108         self.db = self.module.Database(config, 'admin')
2109         a = self.module.Class(self.db, "a", name=String(), newstr=String())
2110         self.db.post_init()
2112         aid2 = self.db.a.create(name='apple', newstr='booz')
2113         self.db.commit()
2116     def init_amodml(self):
2117         self.open_database()
2118         a = self.module.Class(self.db, "a", name=String(),
2119             newml=Multilink('a'))
2120         a.setkey('name')
2121         self.db.post_init()
2123     def test_makeNewMultilink(self):
2124         self.init_a()
2125         aid = self.db.a.create(name='apple')
2126         self.assertEqual(self.db.a.lookup('apple'), aid)
2127         self.db.commit(); self.db.close()
2129         # add a multilink prop
2130         self.init_amodml()
2131         bid = self.db.a.create(name='bear', newml=[aid])
2132         self.assertEqual(self.db.a.find(newml=aid), [bid])
2133         self.assertEqual(self.db.a.lookup('apple'), aid)
2134         self.db.commit(); self.db.close()
2136         # check
2137         self.init_amodml()
2138         self.assertEqual(self.db.a.find(newml=aid), [bid])
2139         self.assertEqual(self.db.a.lookup('apple'), aid)
2140         self.assertEqual(self.db.a.lookup('bear'), bid)
2142         # confirm journal's ok
2143         self.db.getjournal('a', aid)
2144         self.db.getjournal('a', bid)
2146     def test_removeMultilink(self):
2147         # add a multilink prop
2148         self.init_amodml()
2149         aid = self.db.a.create(name='apple')
2150         bid = self.db.a.create(name='bear', newml=[aid])
2151         self.assertEqual(self.db.a.find(newml=aid), [bid])
2152         self.assertEqual(self.db.a.lookup('apple'), aid)
2153         self.assertEqual(self.db.a.lookup('bear'), bid)
2154         self.db.commit(); self.db.close()
2156         # remove the multilink
2157         self.init_a()
2158         self.assertEqual(self.db.a.lookup('apple'), aid)
2159         self.assertEqual(self.db.a.lookup('bear'), bid)
2161         # confirm journal's ok
2162         self.db.getjournal('a', aid)
2163         self.db.getjournal('a', bid)
2165     def test_removeClass(self):
2166         self.init_ab()
2167         aid = self.db.a.create(name='apple')
2168         bid = self.db.b.create(name='bear')
2169         self.db.commit(); self.db.close()
2171         # drop the b class
2172         self.init_a()
2173         self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
2174         self.assertEqual(self.db.a.lookup('apple'), aid)
2175         self.db.commit(); self.db.close()
2177         # now check we can recall the added class' items
2178         self.init_a()
2179         self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
2180         self.assertEqual(self.db.a.lookup('apple'), aid)
2182         # confirm journal's ok
2183         self.db.getjournal('a', aid)
2185 class RDBMSTest:
2186     """ tests specific to RDBMS backends """
2187     def test_indexTest(self):
2188         self.assertEqual(self.db.sql_index_exists('_issue', '_issue_id_idx'), 1)
2189         self.assertEqual(self.db.sql_index_exists('_issue', '_issue_x_idx'), 0)
2191 class FilterCacheTest(commonDBTest):
2192     def testFilteringTransitiveLinkCache(self):
2193         ae, filter, filter_iter = self.filteringSetupTransitiveSearch()
2194         ae, ufilter, ufilter_iter = self.iterSetup('user')
2195         # Need to make ceo his own (and first two users') supervisor
2196         self.db.user.set('1', supervisor = '3')
2197         self.db.user.set('2', supervisor = '3')
2198         self.db.user.set('3', supervisor = '3')
2199         # test bool value
2200         self.db.user.set('4', assignable = True)
2201         self.db.user.set('3', assignable = False)
2202         filt = self.db.issue.filter_iter
2203         ufilt = self.db.user.filter_iter
2204         user_result = \
2205             {  '1' : {'username': 'admin', 'assignable': None,
2206                       'supervisor': '3', 'realname': None, 'roles': 'Admin',
2207                       'creator': '1', 'age': None, 'actor': '1',
2208                       'address': None}
2209             ,  '2' : {'username': 'fred', 'assignable': None,
2210                       'supervisor': '3', 'realname': None, 'roles': 'User',
2211                       'creator': '1', 'age': None, 'actor': '1',
2212                       'address': 'fred@example.com'}
2213             ,  '3' : {'username': 'ceo', 'assignable': False,
2214                       'supervisor': '3', 'realname': None, 'roles': None,
2215                       'creator': '1', 'age': 129.0, 'actor': '1',
2216                       'address': None}
2217             ,  '4' : {'username': 'grouplead1', 'assignable': True,
2218                       'supervisor': '3', 'realname': None, 'roles': None,
2219                       'creator': '1', 'age': 29.0, 'actor': '1',
2220                       'address': None}
2221             ,  '5' : {'username': 'grouplead2', 'assignable': None,
2222                       'supervisor': '3', 'realname': None, 'roles': None,
2223                       'creator': '1', 'age': 29.0, 'actor': '1',
2224                       'address': None}
2225             ,  '6' : {'username': 'worker1', 'assignable': None,
2226                       'supervisor': '4', 'realname': None, 'roles': None,
2227                       'creator': '1', 'age': 25.0, 'actor': '1',
2228                       'address': None}
2229             ,  '7' : {'username': 'worker2', 'assignable': None,
2230                       'supervisor': '4', 'realname': None, 'roles': None,
2231                       'creator': '1', 'age': 24.0, 'actor': '1',
2232                       'address': None}
2233             ,  '8' : {'username': 'worker3', 'assignable': None,
2234                       'supervisor': '5', 'realname': None, 'roles': None,
2235                       'creator': '1', 'age': 23.0, 'actor': '1',
2236                       'address': None}
2237             ,  '9' : {'username': 'worker4', 'assignable': None,
2238                       'supervisor': '5', 'realname': None, 'roles': None,
2239                       'creator': '1', 'age': 22.0, 'actor': '1',
2240                       'address': None}
2241             , '10' : {'username': 'worker5', 'assignable': None,
2242                       'supervisor': '5', 'realname': None, 'roles': None,
2243                       'creator': '1', 'age': 21.0, 'actor': '1',
2244                       'address': None}
2245             }
2246         foo = date.Interval('-1d')
2247         issue_result = \
2248             { '1' : {'title': 'ts1', 'status': '2', 'assignedto': '6',
2249                      'priority': '3', 'messages' : ['4'], 'nosy' : ['4']}
2250             , '2' : {'title': 'ts2', 'status': '1', 'assignedto': '6',
2251                      'priority': '3', 'messages' : ['4'], 'nosy' : ['5']}
2252             , '3' : {'title': 'ts4', 'status': '2', 'assignedto': '7',
2253                      'priority': '3', 'messages' : ['5']}
2254             , '4' : {'title': 'ts5', 'status': '1', 'assignedto': '8',
2255                      'priority': '3', 'messages' : ['6']}
2256             , '5' : {'title': 'ts6', 'status': '2', 'assignedto': '9',
2257                      'priority': '3', 'messages' : ['7']}
2258             , '6' : {'title': 'ts7', 'status': '1', 'assignedto': '10',
2259                      'priority': '3', 'messages' : ['8'], 'foo' : None}
2260             , '7' : {'title': 'ts8', 'status': '2', 'assignedto': '10',
2261                      'priority': '3', 'messages' : ['8'], 'foo' : foo}
2262             , '8' : {'title': 'ts9', 'status': '1', 'assignedto': '10',
2263                      'priority': '3', 'messages' : ['7', '8']}
2264             }
2265         result = []
2266         self.db.clearCache()
2267         for id in ufilt(None, {}, [('+','supervisor.supervisor.supervisor'),
2268             ('-','supervisor.supervisor'), ('-','supervisor'),
2269             ('+','username')]):
2270             result.append(id)
2271             nodeid = id
2272             for x in range(4):
2273                 assert(('user', nodeid) in self.db.cache)
2274                 n = self.db.user.getnode(nodeid)
2275                 for k, v in user_result[nodeid].iteritems():
2276                     ae((k, n[k]), (k, v))
2277                 for k in 'creation', 'activity':
2278                     assert(n[k])
2279                 nodeid = n.supervisor
2280             self.db.clearCache()
2281         ae (result, ['8', '9', '10', '6', '7', '1', '3', '2', '4', '5'])
2283         result = []
2284         self.db.clearCache()
2285         for id in filt(None, {},
2286             [('+','assignedto.supervisor.supervisor.supervisor'),
2287             ('+','assignedto.supervisor.supervisor'),
2288             ('-','assignedto.supervisor'), ('+','assignedto')]):
2289             result.append(id)
2290             assert(('issue', id) in self.db.cache)
2291             n = self.db.issue.getnode(id)
2292             for k, v in issue_result[id].iteritems():
2293                 ae((k, n[k]), (k, v))
2294             for k in 'creation', 'activity':
2295                 assert(n[k])
2296             nodeid = n.assignedto
2297             for x in range(4):
2298                 assert(('user', nodeid) in self.db.cache)
2299                 n = self.db.user.getnode(nodeid)
2300                 for k, v in user_result[nodeid].iteritems():
2301                     ae((k, n[k]), (k, v))
2302                 for k in 'creation', 'activity':
2303                     assert(n[k])
2304                 nodeid = n.supervisor
2305             self.db.clearCache()
2306         ae (result, ['4', '5', '6', '7', '8', '1', '2', '3'])
2309 class ClassicInitTest(unittest.TestCase):
2310     count = 0
2311     db = None
2313     def setUp(self):
2314         ClassicInitTest.count = ClassicInitTest.count + 1
2315         self.dirname = '_test_init_%s'%self.count
2316         try:
2317             shutil.rmtree(self.dirname)
2318         except OSError, error:
2319             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
2321     def testCreation(self):
2322         ae = self.assertEqual
2324         # set up and open a tracker
2325         tracker = setupTracker(self.dirname, self.backend)
2326         # open the database
2327         db = self.db = tracker.open('test')
2329         # check the basics of the schema and initial data set
2330         l = db.priority.list()
2331         l.sort()
2332         ae(l, ['1', '2', '3', '4', '5'])
2333         l = db.status.list()
2334         l.sort()
2335         ae(l, ['1', '2', '3', '4', '5', '6', '7', '8'])
2336         l = db.keyword.list()
2337         ae(l, [])
2338         l = db.user.list()
2339         l.sort()
2340         ae(l, ['1', '2'])
2341         l = db.msg.list()
2342         ae(l, [])
2343         l = db.file.list()
2344         ae(l, [])
2345         l = db.issue.list()
2346         ae(l, [])
2348     def tearDown(self):
2349         if self.db is not None:
2350             self.db.close()
2351         try:
2352             shutil.rmtree(self.dirname)
2353         except OSError, error:
2354             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
2356 class ConcurrentDBTest(ClassicInitTest):
2357     def testConcurrency(self):
2358         # The idea here is a read-modify-update cycle in the presence of
2359         # a cache that has to be properly handled. The same applies if
2360         # we extend a String or otherwise modify something that depends
2361         # on the previous value.
2363         # set up and open a tracker
2364         tracker = setupTracker(self.dirname, self.backend)
2365         # open the database
2366         self.db = tracker.open('admin')
2368         prio = '1'
2369         self.assertEqual(self.db.priority.get(prio, 'order'), 1.0)
2370         def inc(db):
2371             db.priority.set(prio, order=db.priority.get(prio, 'order') + 1)
2373         inc(self.db)
2375         db2 = tracker.open("admin")
2376         self.assertEqual(db2.priority.get(prio, 'order'), 1.0)
2377         db2.commit()
2378         self.db.commit()
2379         self.assertEqual(self.db.priority.get(prio, 'order'), 2.0)
2381         inc(db2)
2382         db2.commit()
2383         db2.clearCache()
2384         self.assertEqual(db2.priority.get(prio, 'order'), 3.0)
2385         db2.close()
2388 # vim: set et sts=4 sw=4 :