Code

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