Code

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