Code

- under the heading of "questionable whether it's a fix or not"
[roundup.git] / test / test_db.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: test_db.py,v 1.88 2003-04-22 20:53:55 kedder Exp $ 
20 import unittest, os, shutil, time
22 from roundup.hyperdb import String, Password, Link, Multilink, Date, \
23     Interval, DatabaseError, Boolean, Number, Node
24 from roundup import date, password
25 from roundup.indexer import Indexer
27 def setupSchema(db, create, module):
28     status = module.Class(db, "status", name=String())
29     status.setkey("name")
30     user = module.Class(db, "user", username=String(), password=Password(),
31         assignable=Boolean(), age=Number(), roles=String())
32     user.setkey("username")
33     file = module.FileClass(db, "file", name=String(), type=String(),
34         comment=String(indexme="yes"), fooz=Password())
35     issue = module.IssueClass(db, "issue", title=String(indexme="yes"),
36         status=Link("status"), nosy=Multilink("user"), deadline=Date(),
37         foo=Interval(), files=Multilink("file"), assignedto=Link('user'))
38     session = module.Class(db, 'session', title=String())
39     session.disableJournalling()
40     db.post_init()
41     if create:
42         user.create(username="admin", roles='Admin')
43         status.create(name="unread")
44         status.create(name="in-progress")
45         status.create(name="testing")
46         status.create(name="resolved")
47     db.commit()
49 class MyTestCase(unittest.TestCase):
50     def tearDown(self):
51         self.db.close()
52         if os.path.exists('_test_dir'):
53             shutil.rmtree('_test_dir')
55 class config:
56     DATABASE='_test_dir'
57     MAILHOST = 'localhost'
58     MAIL_DOMAIN = 'fill.me.in.'
59     TRACKER_NAME = 'Roundup issue tracker'
60     TRACKER_EMAIL = 'issue_tracker@%s'%MAIL_DOMAIN
61     TRACKER_WEB = 'http://some.useful.url/'
62     ADMIN_EMAIL = 'roundup-admin@%s'%MAIL_DOMAIN
63     FILTER_POSITION = 'bottom'      # one of 'top', 'bottom', 'top and bottom'
64     ANONYMOUS_ACCESS = 'deny'       # either 'deny' or 'allow'
65     ANONYMOUS_REGISTER = 'deny'     # either 'deny' or 'allow'
66     MESSAGES_TO_AUTHOR = 'no'       # either 'yes' or 'no'
67     EMAIL_SIGNATURE_POSITION = 'bottom'
68     # Mysql connection data
69     MYSQL_DBHOST = 'localhost'
70     MYSQL_DBUSER = 'rounduptest'
71     MYSQL_DBPASSWORD = 'rounduptest'
72     MYSQL_DBNAME = 'rounduptest'
73     MYSQL_DATABASE = (MYSQL_DBHOST, MYSQL_DBUSER, MYSQL_DBPASSWORD, MYSQL_DBNAME)
75 class nodbconfig(config):
76     MYSQL_DATABASE = (config.MYSQL_DBHOST, config.MYSQL_DBUSER, config.MYSQL_DBPASSWORD)
78 class anydbmDBTestCase(MyTestCase):
79     def setUp(self):
80         from roundup.backends import anydbm
81         # remove previous test, ignore errors
82         if os.path.exists(config.DATABASE):
83             shutil.rmtree(config.DATABASE)
84         os.makedirs(config.DATABASE + '/files')
85         self.db = anydbm.Database(config, 'admin')
86         setupSchema(self.db, 1, anydbm)
88     #
89     # schema mutation
90     #
91     def testAddProperty(self):
92         self.db.issue.create(title="spam", status='1')
93         self.db.commit()
95         self.db.issue.addprop(fixer=Link("user"))
96         # force any post-init stuff to happen
97         self.db.post_init()
98         props = self.db.issue.getprops()
99         keys = props.keys()
100         keys.sort()
101         self.assertEqual(keys, ['activity', 'assignedto', 'creation',
102             'creator', 'deadline', 'files', 'fixer', 'foo', 'id', 'messages',
103             'nosy', 'status', 'superseder', 'title'])
104         self.assertEqual(self.db.issue.get('1', "fixer"), None)
106     def testRemoveProperty(self):
107         self.db.issue.create(title="spam", status='1')
108         self.db.commit()
110         del self.db.issue.properties['title']
111         self.db.post_init()
112         props = self.db.issue.getprops()
113         keys = props.keys()
114         keys.sort()
115         self.assertEqual(keys, ['activity', 'assignedto', 'creation',
116             'creator', 'deadline', 'files', 'foo', 'id', 'messages',
117             'nosy', 'status', 'superseder'])
118         self.assertEqual(self.db.issue.list(), ['1'])
120     def testAddRemoveProperty(self):
121         self.db.issue.create(title="spam", status='1')
122         self.db.commit()
124         self.db.issue.addprop(fixer=Link("user"))
125         del self.db.issue.properties['title']
126         self.db.post_init()
127         props = self.db.issue.getprops()
128         keys = props.keys()
129         keys.sort()
130         self.assertEqual(keys, ['activity', 'assignedto', 'creation',
131             'creator', 'deadline', 'files', 'fixer', 'foo', 'id', 'messages',
132             'nosy', 'status', 'superseder'])
133         self.assertEqual(self.db.issue.list(), ['1'])
135     #
136     # basic operations
137     #
138     def testIDGeneration(self):
139         id1 = self.db.issue.create(title="spam", status='1')
140         id2 = self.db.issue.create(title="eggs", status='2')
141         self.assertNotEqual(id1, id2)
143     def testStringChange(self):
144         for commit in (0,1):
145             # test set & retrieve
146             nid = self.db.issue.create(title="spam", status='1')
147             self.assertEqual(self.db.issue.get(nid, 'title'), 'spam')
149             # change and make sure we retrieve the correct value
150             self.db.issue.set(nid, title='eggs')
151             if commit: self.db.commit()
152             self.assertEqual(self.db.issue.get(nid, 'title'), 'eggs')
154     def testStringUnset(self):
155         for commit in (0,1):
156             nid = self.db.issue.create(title="spam", status='1')
157             if commit: self.db.commit()
158             self.assertEqual(self.db.issue.get(nid, 'title'), 'spam')
159             # make sure we can unset
160             self.db.issue.set(nid, title=None)
161             if commit: self.db.commit()
162             self.assertEqual(self.db.issue.get(nid, "title"), None)
164     def testLinkChange(self):
165         for commit in (0,1):
166             nid = self.db.issue.create(title="spam", status='1')
167             if commit: self.db.commit()
168             self.assertEqual(self.db.issue.get(nid, "status"), '1')
169             self.db.issue.set(nid, status='2')
170             if commit: self.db.commit()
171             self.assertEqual(self.db.issue.get(nid, "status"), '2')
173     def testLinkUnset(self):
174         for commit in (0,1):
175             nid = self.db.issue.create(title="spam", status='1')
176             if commit: self.db.commit()
177             self.db.issue.set(nid, status=None)
178             if commit: self.db.commit()
179             self.assertEqual(self.db.issue.get(nid, "status"), None)
181     def testMultilinkChange(self):
182         for commit in (0,1):
183             u1 = self.db.user.create(username='foo%s'%commit)
184             u2 = self.db.user.create(username='bar%s'%commit)
185             nid = self.db.issue.create(title="spam", nosy=[u1])
186             if commit: self.db.commit()
187             self.assertEqual(self.db.issue.get(nid, "nosy"), [u1])
188             self.db.issue.set(nid, nosy=[])
189             if commit: self.db.commit()
190             self.assertEqual(self.db.issue.get(nid, "nosy"), [])
191             self.db.issue.set(nid, nosy=[u1,u2])
192             if commit: self.db.commit()
193             self.assertEqual(self.db.issue.get(nid, "nosy"), [u1,u2])
195     def testDateChange(self):
196         for commit in (0,1):
197             nid = self.db.issue.create(title="spam", status='1')
198             a = self.db.issue.get(nid, "deadline")
199             if commit: self.db.commit()
200             self.db.issue.set(nid, deadline=date.Date())
201             b = self.db.issue.get(nid, "deadline")
202             if commit: self.db.commit()
203             self.assertNotEqual(a, b)
204             self.assertNotEqual(b, date.Date('1970-1-1 00:00:00'))
206     def testDateUnset(self):
207         for commit in (0,1):
208             nid = self.db.issue.create(title="spam", status='1')
209             self.db.issue.set(nid, deadline=date.Date())
210             if commit: self.db.commit()
211             self.assertNotEqual(self.db.issue.get(nid, "deadline"), None)
212             self.db.issue.set(nid, deadline=None)
213             if commit: self.db.commit()
214             self.assertEqual(self.db.issue.get(nid, "deadline"), None)
216     def testIntervalChange(self):
217         for commit in (0,1):
218             nid = self.db.issue.create(title="spam", status='1')
219             if commit: self.db.commit()
220             a = self.db.issue.get(nid, "foo")
221             i = date.Interval('-1d')
222             self.db.issue.set(nid, foo=i)
223             if commit: self.db.commit()
224             self.assertNotEqual(self.db.issue.get(nid, "foo"), a)
225             self.assertEqual(i, self.db.issue.get(nid, "foo"))
226             j = date.Interval('1y')
227             self.db.issue.set(nid, foo=j)
228             if commit: self.db.commit()
229             self.assertNotEqual(self.db.issue.get(nid, "foo"), i)
230             self.assertEqual(j, self.db.issue.get(nid, "foo"))
232     def testIntervalUnset(self):
233         for commit in (0,1):
234             nid = self.db.issue.create(title="spam", status='1')
235             self.db.issue.set(nid, foo=date.Interval('-1d'))
236             if commit: self.db.commit()
237             self.assertNotEqual(self.db.issue.get(nid, "foo"), None)
238             self.db.issue.set(nid, foo=None)
239             if commit: self.db.commit()
240             self.assertEqual(self.db.issue.get(nid, "foo"), None)
242     def testBooleanChange(self):
243         userid = self.db.user.create(username='foo', assignable=1)
244         self.assertEqual(1, self.db.user.get(userid, 'assignable'))
245         self.db.user.set(userid, assignable=0)
246         self.assertEqual(self.db.user.get(userid, 'assignable'), 0)
248     def testBooleanUnset(self):
249         nid = self.db.user.create(username='foo', assignable=1)
250         self.db.user.set(nid, assignable=None)
251         self.assertEqual(self.db.user.get(nid, "assignable"), None)
253     def testNumberChange(self):
254         nid = self.db.user.create(username='foo', age=1)
255         self.assertEqual(1, self.db.user.get(nid, 'age'))
256         self.db.user.set(nid, age=3)
257         self.assertNotEqual(self.db.user.get(nid, 'age'), 1)
258         self.db.user.set(nid, age=1.0)
259         self.assertEqual(self.db.user.get(nid, 'age'), 1)
260         self.db.user.set(nid, age=0)
261         self.assertEqual(self.db.user.get(nid, 'age'), 0)
263         nid = self.db.user.create(username='bar', age=0)
264         self.assertEqual(self.db.user.get(nid, 'age'), 0)
266     def testNumberUnset(self):
267         nid = self.db.user.create(username='foo', age=1)
268         self.db.user.set(nid, age=None)
269         self.assertEqual(self.db.user.get(nid, "age"), None)
271     def testKeyValue(self):
272         newid = self.db.user.create(username="spam")
273         self.assertEqual(self.db.user.lookup('spam'), newid)
274         self.db.commit()
275         self.assertEqual(self.db.user.lookup('spam'), newid)
276         self.db.user.retire(newid)
277         self.assertRaises(KeyError, self.db.user.lookup, 'spam')
279         # use the key again now that the old is retired
280         newid2 = self.db.user.create(username="spam")
281         self.assertNotEqual(newid, newid2)
282         # try to restore old node. this shouldn't succeed!
283         self.assertRaises(KeyError, self.db.user.restore, newid)
285     def testRetire(self):
286         self.db.issue.create(title="spam", status='1')
287         b = self.db.status.get('1', 'name')
288         a = self.db.status.list()
289         self.db.status.retire('1')
290         # make sure the list is different 
291         self.assertNotEqual(a, self.db.status.list())
292         # can still access the node if necessary
293         self.assertEqual(self.db.status.get('1', 'name'), b)
294         self.db.commit()
295         self.assertEqual(self.db.status.get('1', 'name'), b)
296         self.assertNotEqual(a, self.db.status.list())
297         # try to restore retired node
298         self.db.status.restore('1')
299         self.assertEqual(a, self.db.status.list())
301     def testSerialisation(self):
302         nid = self.db.issue.create(title="spam", status='1',
303             deadline=date.Date(), foo=date.Interval('-1d'))
304         self.db.commit()
305         assert isinstance(self.db.issue.get(nid, 'deadline'), date.Date)
306         assert isinstance(self.db.issue.get(nid, 'foo'), date.Interval)
307         uid = self.db.user.create(username="fozzy",
308             password=password.Password('t. bear'))
309         self.db.commit()
310         assert isinstance(self.db.user.get(uid, 'password'), password.Password)
312     def testTransactions(self):
313         # remember the number of items we started
314         num_issues = len(self.db.issue.list())
315         num_files = self.db.numfiles()
316         self.db.issue.create(title="don't commit me!", status='1')
317         self.assertNotEqual(num_issues, len(self.db.issue.list()))
318         self.db.rollback()
319         self.assertEqual(num_issues, len(self.db.issue.list()))
320         self.db.issue.create(title="please commit me!", status='1')
321         self.assertNotEqual(num_issues, len(self.db.issue.list()))
322         self.db.commit()
323         self.assertNotEqual(num_issues, len(self.db.issue.list()))
324         self.db.rollback()
325         self.assertNotEqual(num_issues, len(self.db.issue.list()))
326         self.db.file.create(name="test", type="text/plain", content="hi")
327         self.db.rollback()
328         self.assertEqual(num_files, self.db.numfiles())
329         for i in range(10):
330             self.db.file.create(name="test", type="text/plain", 
331                     content="hi %d"%(i))
332             self.db.commit()
333         num_files2 = self.db.numfiles()
334         self.assertNotEqual(num_files, num_files2)
335         self.db.file.create(name="test", type="text/plain", content="hi")
336         self.db.rollback()
337         self.assertNotEqual(num_files, self.db.numfiles())
338         self.assertEqual(num_files2, self.db.numfiles())
340         # rollback / cache interaction
341         name1 = self.db.user.get('1', 'username')
342         self.db.user.set('1', username = name1+name1)
343         # get the prop so the info's forced into the cache (if there is one)
344         self.db.user.get('1', 'username')
345         self.db.rollback()
346         name2 = self.db.user.get('1', 'username')
347         self.assertEqual(name1, name2)
349     def testDestroyNoJournalling(self):
350         self.innerTestDestroy(klass=self.db.session)
352     def testDestroyJournalling(self):
353         self.innerTestDestroy(klass=self.db.issue)
355     def innerTestDestroy(self, klass):
356         newid = klass.create(title='Mr Friendly')
357         n = len(klass.list())
358         self.assertEqual(klass.get(newid, 'title'), 'Mr Friendly')
359         klass.destroy(newid)
360         self.assertRaises(IndexError, klass.get, newid, 'title')
361         self.assertNotEqual(len(klass.list()), n)
362         if klass.do_journal:
363             self.assertRaises(IndexError, klass.history, newid)
365         # now with a commit
366         newid = klass.create(title='Mr Friendly')
367         n = len(klass.list())
368         self.assertEqual(klass.get(newid, 'title'), 'Mr Friendly')
369         self.db.commit()
370         klass.destroy(newid)
371         self.assertRaises(IndexError, klass.get, newid, 'title')
372         self.db.commit()
373         self.assertRaises(IndexError, klass.get, newid, 'title')
374         self.assertNotEqual(len(klass.list()), n)
375         if klass.do_journal:
376             self.assertRaises(IndexError, klass.history, newid)
378         # now with a rollback
379         newid = klass.create(title='Mr Friendly')
380         n = len(klass.list())
381         self.assertEqual(klass.get(newid, 'title'), 'Mr Friendly')
382         self.db.commit()
383         klass.destroy(newid)
384         self.assertNotEqual(len(klass.list()), n)
385         self.assertRaises(IndexError, klass.get, newid, 'title')
386         self.db.rollback()
387         self.assertEqual(klass.get(newid, 'title'), 'Mr Friendly')
388         self.assertEqual(len(klass.list()), n)
389         if klass.do_journal:
390             self.assertNotEqual(klass.history(newid), [])
392     def testExceptions(self):
393         # this tests the exceptions that should be raised
394         ar = self.assertRaises
396         #
397         # class create
398         #
399         # string property
400         ar(TypeError, self.db.status.create, name=1)
401         # invalid property name
402         ar(KeyError, self.db.status.create, foo='foo')
403         # key name clash
404         ar(ValueError, self.db.status.create, name='unread')
405         # invalid link index
406         ar(IndexError, self.db.issue.create, title='foo', status='bar')
407         # invalid link value
408         ar(ValueError, self.db.issue.create, title='foo', status=1)
409         # invalid multilink type
410         ar(TypeError, self.db.issue.create, title='foo', status='1',
411             nosy='hello')
412         # invalid multilink index type
413         ar(ValueError, self.db.issue.create, title='foo', status='1',
414             nosy=[1])
415         # invalid multilink index
416         ar(IndexError, self.db.issue.create, title='foo', status='1',
417             nosy=['10'])
419         #
420         # key property
421         # 
422         # key must be a String
423         ar(TypeError, self.db.file.setkey, 'fooz')
424         # key must exist
425         ar(KeyError, self.db.file.setkey, 'fubar')
427         #
428         # class get
429         #
430         # invalid node id
431         ar(IndexError, self.db.issue.get, '99', 'title')
432         # invalid property name
433         ar(KeyError, self.db.status.get, '2', 'foo')
435         #
436         # class set
437         #
438         # invalid node id
439         ar(IndexError, self.db.issue.set, '99', title='foo')
440         # invalid property name
441         ar(KeyError, self.db.status.set, '1', foo='foo')
442         # string property
443         ar(TypeError, self.db.status.set, '1', name=1)
444         # key name clash
445         ar(ValueError, self.db.status.set, '2', name='unread')
446         # set up a valid issue for me to work on
447         id = self.db.issue.create(title="spam", status='1')
448         # invalid link index
449         ar(IndexError, self.db.issue.set, id, title='foo', status='bar')
450         # invalid link value
451         ar(ValueError, self.db.issue.set, id, title='foo', status=1)
452         # invalid multilink type
453         ar(TypeError, self.db.issue.set, id, title='foo', status='1',
454             nosy='hello')
455         # invalid multilink index type
456         ar(ValueError, self.db.issue.set, id, title='foo', status='1',
457             nosy=[1])
458         # invalid multilink index
459         ar(IndexError, self.db.issue.set, id, title='foo', status='1',
460             nosy=['10'])
461         # NOTE: the following increment the username to avoid problems
462         # within metakit's backend (it creates the node, and then sets the
463         # info, so the create (and by a fluke the username set) go through
464         # before the age/assignable/etc. set, which raises the exception)
465         # invalid number value
466         ar(TypeError, self.db.user.create, username='foo', age='a')
467         # invalid boolean value
468         ar(TypeError, self.db.user.create, username='foo2', assignable='true')
469         nid = self.db.user.create(username='foo3')
470         # invalid number value
471         ar(TypeError, self.db.user.set, nid, age='a')
472         # invalid boolean value
473         ar(TypeError, self.db.user.set, nid, assignable='true')
475     def testJournals(self):
476         self.db.user.create(username="mary")
477         self.db.user.create(username="pete")
478         self.db.issue.create(title="spam", status='1')
479         self.db.commit()
481         # journal entry for issue create
482         journal = self.db.getjournal('issue', '1')
483         self.assertEqual(1, len(journal))
484         (nodeid, date_stamp, journaltag, action, params) = journal[0]
485         self.assertEqual(nodeid, '1')
486         self.assertEqual(journaltag, self.db.user.lookup('admin'))
487         self.assertEqual(action, 'create')
488         keys = params.keys()
489         keys.sort()
490         self.assertEqual(keys, [])
492         # journal entry for link
493         journal = self.db.getjournal('user', '1')
494         self.assertEqual(1, len(journal))
495         self.db.issue.set('1', assignedto='1')
496         self.db.commit()
497         journal = self.db.getjournal('user', '1')
498         self.assertEqual(2, len(journal))
499         (nodeid, date_stamp, journaltag, action, params) = journal[1]
500         self.assertEqual('1', nodeid)
501         self.assertEqual('1', journaltag)
502         self.assertEqual('link', action)
503         self.assertEqual(('issue', '1', 'assignedto'), params)
505         # journal entry for unlink
506         self.db.issue.set('1', assignedto='2')
507         self.db.commit()
508         journal = self.db.getjournal('user', '1')
509         self.assertEqual(3, len(journal))
510         (nodeid, date_stamp, journaltag, action, params) = journal[2]
511         self.assertEqual('1', nodeid)
512         self.assertEqual('1', journaltag)
513         self.assertEqual('unlink', action)
514         self.assertEqual(('issue', '1', 'assignedto'), params)
516         # test disabling journalling
517         # ... get the last entry
518         time.sleep(1)
519         entry = self.db.getjournal('issue', '1')[-1]
520         (x, date_stamp, x, x, x) = entry
521         self.db.issue.disableJournalling()
522         self.db.issue.set('1', title='hello world')
523         self.db.commit()
524         entry = self.db.getjournal('issue', '1')[-1]
525         (x, date_stamp2, x, x, x) = entry
526         # see if the change was journalled when it shouldn't have been
527         self.assertEqual(date_stamp, date_stamp2)
528         time.sleep(1)
529         self.db.issue.enableJournalling()
530         self.db.issue.set('1', title='hello world 2')
531         self.db.commit()
532         entry = self.db.getjournal('issue', '1')[-1]
533         (x, date_stamp2, x, x, x) = entry
534         # see if the change was journalled
535         self.assertNotEqual(date_stamp, date_stamp2)
537     def testJournalPreCommit(self):
538         id = self.db.user.create(username="mary")
539         self.assertEqual(len(self.db.getjournal('user', id)), 1)
540         self.db.commit()
542     def testPack(self):
543         id = self.db.issue.create(title="spam", status='1')
544         self.db.commit()
545         self.db.issue.set(id, status='2')
546         self.db.commit()
548         # sleep for at least a second, then get a date to pack at
549         time.sleep(1)
550         pack_before = date.Date('.')
552         # wait another second and add one more entry
553         time.sleep(1)
554         self.db.issue.set(id, status='3')
555         self.db.commit()
556         jlen = len(self.db.getjournal('issue', id))
558         # pack
559         self.db.pack(pack_before)
561         # we should have the create and last set entries now
562         self.assertEqual(jlen-1, len(self.db.getjournal('issue', id)))
564     def testSearching(self):
565         self.db.file.create(content='hello', type="text/plain")
566         self.db.file.create(content='world', type="text/frozz",
567             comment='blah blah')
568         self.db.issue.create(files=['1', '2'], title="flebble plop")
569         self.db.issue.create(title="flebble frooz")
570         self.db.commit()
571         self.assertEquals(self.db.indexer.search(['hello'], self.db.issue),
572             {'1': {'files': ['1']}})
573         self.assertEquals(self.db.indexer.search(['world'], self.db.issue), {})
574         self.assertEquals(self.db.indexer.search(['frooz'], self.db.issue),
575             {'2': {}})
576         self.assertEquals(self.db.indexer.search(['flebble'], self.db.issue),
577             {'2': {}, '1': {}})
579     def testReindexing(self):
580         self.db.issue.create(title="frooz")
581         self.db.commit()
582         self.assertEquals(self.db.indexer.search(['frooz'], self.db.issue),
583             {'1': {}})
584         self.db.issue.set('1', title="dooble")
585         self.db.commit()
586         self.assertEquals(self.db.indexer.search(['dooble'], self.db.issue),
587             {'1': {}})
588         self.assertEquals(self.db.indexer.search(['frooz'], self.db.issue), {})
590     def testForcedReindexing(self):
591         self.db.issue.create(title="flebble frooz")
592         self.db.commit()
593         self.assertEquals(self.db.indexer.search(['flebble'], self.db.issue),
594             {'1': {}})
595         self.db.indexer.quiet = 1
596         self.db.indexer.force_reindex()
597         self.db.post_init()
598         self.db.indexer.quiet = 9
599         self.assertEquals(self.db.indexer.search(['flebble'], self.db.issue),
600             {'1': {}})
602     #
603     # searching tests follow
604     #
605     def testFind(self):
606         self.db.user.create(username='test')
607         ids = []
608         ids.append(self.db.issue.create(status="1", nosy=['1']))
609         oddid = self.db.issue.create(status="2", nosy=['2'], assignedto='2')
610         ids.append(self.db.issue.create(status="1", nosy=['1','2']))
611         self.db.issue.create(status="3", nosy=['1'], assignedto='1')
612         ids.sort()
614         # should match first and third
615         got = self.db.issue.find(status='1')
616         got.sort()
617         self.assertEqual(got, ids)
619         # none
620         self.assertEqual(self.db.issue.find(status='4'), [])
622         # should match first and third
623         got = self.db.issue.find(assignedto=None)
624         got.sort()
625         self.assertEqual(got, ids)
627         # should match first three
628         got = self.db.issue.find(status='1', nosy='2')
629         got.sort()
630         ids.append(oddid)
631         ids.sort()
632         self.assertEqual(got, ids)
634         # none
635         self.assertEqual(self.db.issue.find(status='4', nosy='3'), [])
637     def testStringFind(self):
638         ids = []
639         ids.append(self.db.issue.create(title="spam"))
640         self.db.issue.create(title="not spam")
641         ids.append(self.db.issue.create(title="spam"))
642         ids.sort()
643         got = self.db.issue.stringFind(title='spam')
644         got.sort()
645         self.assertEqual(got, ids)
646         self.assertEqual(self.db.issue.stringFind(title='fubar'), [])
648     def filteringSetup(self):
649         for user in (
650                 {'username': 'bleep'},
651                 {'username': 'blop'},
652                 {'username': 'blorp'}):
653             self.db.user.create(**user)
654         iss = self.db.issue
655         for issue in (
656                 {'title': 'issue one', 'status': '2',
657                     'foo': date.Interval('1:10'), 
658                     'deadline': date.Date('2003-01-01.00:00')},
659                 {'title': 'issue two', 'status': '1',
660                     'foo': date.Interval('1d'), 
661                     'deadline': date.Date('2003-02-16.22:50')},
662                 {'title': 'issue three', 'status': '1',
663                     'nosy': ['1','2'], 'deadline': date.Date('2003-02-18')},
664                 {'title': 'non four', 'status': '3',
665                     'foo': date.Interval('0:10'), 
666                     'nosy': ['1'], 'deadline': date.Date('2004-03-08')}):
667             self.db.issue.create(**issue)
668         self.db.commit()
669         return self.assertEqual, self.db.issue.filter
671     def testFilteringID(self):
672         ae, filt = self.filteringSetup()
673         ae(filt(None, {'id': '1'}, ('+','id'), (None,None)), ['1'])
675     def testFilteringString(self):
676         ae, filt = self.filteringSetup()
677         ae(filt(None, {'title': 'issue one'}, ('+','id'), (None,None)), ['1'])
678         ae(filt(None, {'title': 'issue'}, ('+','id'), (None,None)),
679             ['1','2','3'])
681     def testFilteringLink(self):
682         ae, filt = self.filteringSetup()
683         ae(filt(None, {'status': '1'}, ('+','id'), (None,None)), ['2','3'])
685     def testFilteringMultilink(self):
686         ae, filt = self.filteringSetup()
687         ae(filt(None, {'nosy': '2'}, ('+','id'), (None,None)), ['3'])
688         ae(filt(None, {'nosy': '-1'}, ('+','id'), (None,None)), ['1', '2'])
690     def testFilteringMany(self):
691         ae, filt = self.filteringSetup()
692         ae(filt(None, {'nosy': '2', 'status': '1'}, ('+','id'), (None,None)),
693             ['3'])
695     def testFilteringRange(self):
696         ae, filt = self.filteringSetup()
697         # Date ranges
698         ae(filt(None, {'deadline': 'from 2003-02-10 to 2003-02-23'}), ['2','3'])
699         ae(filt(None, {'deadline': '2003-02-10; 2003-02-23'}), ['2','3'])
700         ae(filt(None, {'deadline': '; 2003-02-16'}), ['1'])
701         # Lets assume people won't invent a time machine, otherwise this test
702         # may fail :)
703         ae(filt(None, {'deadline': 'from 2003-02-16'}), ['2', '3', '4'])
704         ae(filt(None, {'deadline': '2003-02-16;'}), ['2', '3', '4'])
705         # year and month granularity
706         ae(filt(None, {'deadline': '2002'}), [])
707         ae(filt(None, {'deadline': '2003'}), ['1', '2', '3'])
708         ae(filt(None, {'deadline': '2004'}), ['4'])
709         ae(filt(None, {'deadline': '2003-02'}), ['2', '3'])
710         ae(filt(None, {'deadline': '2003-03'}), [])
711         ae(filt(None, {'deadline': '2003-02-16'}), ['2'])
712         ae(filt(None, {'deadline': '2003-02-17'}), [])
713         # Interval ranges
714         ae(filt(None, {'foo': 'from 0:50 to 2:00'}), ['1'])
715         ae(filt(None, {'foo': 'from 0:50 to 1d 2:00'}), ['1', '2'])
716         ae(filt(None, {'foo': 'from 5:50'}), ['2'])
717         ae(filt(None, {'foo': 'to 0:05'}), [])
719     def testFilteringIntervalSort(self):
720         ae, filt = self.filteringSetup()
721         # ascending should sort None, 1:10, 1d
722         ae(filt(None, {}, ('+','foo'), (None,None)), ['3', '4', '1', '2'])
723         # descending should sort 1d, 1:10, None
724         ae(filt(None, {}, ('-','foo'), (None,None)), ['2', '1', '4', '3'])
726 # XXX add sorting tests for other types
727 # XXX test auditors and reactors
729 class anydbmReadOnlyDBTestCase(MyTestCase):
730     def setUp(self):
731         from roundup.backends import anydbm
732         # remove previous test, ignore errors
733         if os.path.exists(config.DATABASE):
734             shutil.rmtree(config.DATABASE)
735         os.makedirs(config.DATABASE + '/files')
736         db = anydbm.Database(config, 'admin')
737         setupSchema(db, 1, anydbm)
738         db.close()
739         self.db = anydbm.Database(config)
740         setupSchema(self.db, 0, anydbm)
742     def testExceptions(self):
743         # this tests the exceptions that should be raised
744         ar = self.assertRaises
746         # this tests the exceptions that should be raised
747         ar(DatabaseError, self.db.status.create, name="foo")
748         ar(DatabaseError, self.db.status.set, '1', name="foo")
749         ar(DatabaseError, self.db.status.retire, '1')
752 class bsddbDBTestCase(anydbmDBTestCase):
753     def setUp(self):
754         from roundup.backends import bsddb
755         # remove previous test, ignore errors
756         if os.path.exists(config.DATABASE):
757             shutil.rmtree(config.DATABASE)
758         os.makedirs(config.DATABASE + '/files')
759         self.db = bsddb.Database(config, 'admin')
760         setupSchema(self.db, 1, bsddb)
762 class bsddbReadOnlyDBTestCase(anydbmReadOnlyDBTestCase):
763     def setUp(self):
764         from roundup.backends import bsddb
765         # remove previous test, ignore errors
766         if os.path.exists(config.DATABASE):
767             shutil.rmtree(config.DATABASE)
768         os.makedirs(config.DATABASE + '/files')
769         db = bsddb.Database(config, 'admin')
770         setupSchema(db, 1, bsddb)
771         db.close()
772         self.db = bsddb.Database(config)
773         setupSchema(self.db, 0, bsddb)
776 class bsddb3DBTestCase(anydbmDBTestCase):
777     def setUp(self):
778         from roundup.backends import bsddb3
779         # remove previous test, ignore errors
780         if os.path.exists(config.DATABASE):
781             shutil.rmtree(config.DATABASE)
782         os.makedirs(config.DATABASE + '/files')
783         self.db = bsddb3.Database(config, 'admin')
784         setupSchema(self.db, 1, bsddb3)
786 class bsddb3ReadOnlyDBTestCase(anydbmReadOnlyDBTestCase):
787     def setUp(self):
788         from roundup.backends import bsddb3
789         # remove previous test, ignore errors
790         if os.path.exists(config.DATABASE):
791             shutil.rmtree(config.DATABASE)
792         os.makedirs(config.DATABASE + '/files')
793         db = bsddb3.Database(config, 'admin')
794         setupSchema(db, 1, bsddb3)
795         db.close()
796         self.db = bsddb3.Database(config)
797         setupSchema(self.db, 0, bsddb3)
800 class mysqlDBTestCase(anydbmDBTestCase):
801     def setUp(self):
802         from roundup.backends import mysql
803         # remove previous test, ignore errors
804         if os.path.exists(config.DATABASE):
805             shutil.rmtree(config.DATABASE)
806         os.makedirs(config.DATABASE + '/files')
807         # open database for testing
808         self.db = mysql.Database(config, 'admin')       
809         setupSchema(self.db, 1, mysql)
810          
811     def tearDown(self):
812         from roundup.backends import mysql
813         self.db.close()
814         mysql.Database.nuke(config)
816 class mysqlReadOnlyDBTestCase(anydbmReadOnlyDBTestCase):
817     def setUp(self):
818         from roundup.backends import mysql
819         # remove previous test, ignore errors
820         if os.path.exists(config.DATABASE):
821             shutil.rmtree(config.DATABASE)
822         os.makedirs(config.DATABASE + '/files')
823         self.db = mysql.Database(config)
824         setupSchema(self.db, 0, mysql)
826     def tearDown(self):
827         from roundup.backends import mysql
828         self.db.close()
829         mysql.Database.nuke(config)
831 class sqliteDBTestCase(anydbmDBTestCase):
832     def setUp(self):
833         from roundup.backends import sqlite
834         # remove previous test, ignore errors
835         if os.path.exists(config.DATABASE):
836             shutil.rmtree(config.DATABASE)
837         os.makedirs(config.DATABASE + '/files')
838         self.db = sqlite.Database(config, 'admin')
839         setupSchema(self.db, 1, sqlite)
841 class sqliteReadOnlyDBTestCase(anydbmReadOnlyDBTestCase):
842     def setUp(self):
843         from roundup.backends import sqlite
844         # remove previous test, ignore errors
845         if os.path.exists(config.DATABASE):
846             shutil.rmtree(config.DATABASE)
847         os.makedirs(config.DATABASE + '/files')
848         db = sqlite.Database(config, 'admin')
849         setupSchema(db, 1, sqlite)
850         db.close()
851         self.db = sqlite.Database(config)
852         setupSchema(self.db, 0, sqlite)
855 class metakitDBTestCase(anydbmDBTestCase):
856     def setUp(self):
857         from roundup.backends import metakit
858         import weakref
859         metakit._instances = weakref.WeakValueDictionary()
860         # remove previous test, ignore errors
861         if os.path.exists(config.DATABASE):
862             shutil.rmtree(config.DATABASE)
863         os.makedirs(config.DATABASE + '/files')
864         self.db = metakit.Database(config, 'admin')
865         setupSchema(self.db, 1, metakit)
867     def testTransactions(self):
868         # remember the number of items we started
869         num_issues = len(self.db.issue.list())
870         self.db.issue.create(title="don't commit me!", status='1')
871         self.assertNotEqual(num_issues, len(self.db.issue.list()))
872         self.db.rollback()
873         self.assertEqual(num_issues, len(self.db.issue.list()))
874         self.db.issue.create(title="please commit me!", status='1')
875         self.assertNotEqual(num_issues, len(self.db.issue.list()))
876         self.db.commit()
877         self.assertNotEqual(num_issues, len(self.db.issue.list()))
878         self.db.rollback()
879         self.assertNotEqual(num_issues, len(self.db.issue.list()))
880         self.db.file.create(name="test", type="text/plain", content="hi")
881         self.db.rollback()
882         num_files = len(self.db.file.list())
883         for i in range(10):
884             self.db.file.create(name="test", type="text/plain", 
885                     content="hi %d"%(i))
886             self.db.commit()
887         # TODO: would be good to be able to ensure the file is not on disk after
888         # a rollback...
889         num_files2 = len(self.db.file.list())
890         self.assertNotEqual(num_files, num_files2)
891         self.db.file.create(name="test", type="text/plain", content="hi")
892         num_rfiles = len(os.listdir(self.db.config.DATABASE + '/files/file/0'))
893         self.db.rollback()
894         num_rfiles2 = len(os.listdir(self.db.config.DATABASE + '/files/file/0'))
895         self.assertEqual(num_files2, len(self.db.file.list()))
896         self.assertEqual(num_rfiles2, num_rfiles-1)
898     def testBooleanUnset(self):
899         # XXX: metakit can't unset Booleans :(
900         nid = self.db.user.create(username='foo', assignable=1)
901         self.db.user.set(nid, assignable=None)
902         self.assertEqual(self.db.user.get(nid, "assignable"), 0)
904     def testNumberUnset(self):
905         # XXX: metakit can't unset Numbers :(
906         nid = self.db.user.create(username='foo', age=1)
907         self.db.user.set(nid, age=None)
908         self.assertEqual(self.db.user.get(nid, "age"), 0)
910 class metakitReadOnlyDBTestCase(anydbmReadOnlyDBTestCase):
911     def setUp(self):
912         from roundup.backends import metakit
913         import weakref
914         metakit._instances = weakref.WeakValueDictionary()
915         # remove previous test, ignore errors
916         if os.path.exists(config.DATABASE):
917             shutil.rmtree(config.DATABASE)
918         os.makedirs(config.DATABASE + '/files')
919         db = metakit.Database(config, 'admin')
920         setupSchema(db, 1, metakit)
921         db.close()
922         self.db = metakit.Database(config)
923         setupSchema(self.db, 0, metakit)
925 def suite():
926     p = []
928     l = [
929          unittest.makeSuite(anydbmDBTestCase, 'test'),
930          unittest.makeSuite(anydbmReadOnlyDBTestCase, 'test')
931     ]
932     p.append('anydbm')
933 #    return unittest.TestSuite(l)
935     from roundup import backends
936     if hasattr(backends, 'mysql'):
937         from roundup.backends import mysql
938         try:
939             # Check if we can run mysql tests
940             import MySQLdb
941             db = mysql.Database(nodbconfig, 'admin')
942             db.conn.select_db(config.MYSQL_DBNAME)
943             db.sql("SHOW TABLES");
944             tables = db.sql_fetchall()
945             if tables:
946                 # Database should be empty. We don't dare to delete any data
947                 raise DatabaseError, "(Database %s contains tables)" % config.MYSQL_DBNAME
948             db.sql("DROP DATABASE %s" % config.MYSQL_DBNAME)
949             db.sql("CREATE DATABASE %s" % config.MYSQL_DBNAME)
950             db.close()
951         except (MySQLdb.ProgrammingError, DatabaseError), msg:
952             print "Warning! Mysql tests will not be performed", msg
953             print "See doc/mysql.txt for more details."
954         else:
955             p.append('mysql')
956             l.append(unittest.makeSuite(mysqlDBTestCase, 'test'))
957             l.append(unittest.makeSuite(mysqlReadOnlyDBTestCase, 'test'))
958     #return unittest.TestSuite(l)
960     if hasattr(backends, 'sqlite'):
961         p.append('sqlite')
962         l.append(unittest.makeSuite(sqliteDBTestCase, 'test'))
963         l.append(unittest.makeSuite(sqliteReadOnlyDBTestCase, 'test'))
965     if hasattr(backends, 'bsddb'):
966         p.append('bsddb')
967         l.append(unittest.makeSuite(bsddbDBTestCase, 'test'))
968         l.append(unittest.makeSuite(bsddbReadOnlyDBTestCase, 'test'))
970     if hasattr(backends, 'bsddb3'):
971         p.append('bsddb3')
972         l.append(unittest.makeSuite(bsddb3DBTestCase, 'test'))
973         l.append(unittest.makeSuite(bsddb3ReadOnlyDBTestCase, 'test'))
975     if hasattr(backends, 'metakit'):
976         p.append('metakit')
977         l.append(unittest.makeSuite(metakitDBTestCase, 'test'))
978         l.append(unittest.makeSuite(metakitReadOnlyDBTestCase, 'test'))
980     print 'running %s backend tests'%(', '.join(p))
981     return unittest.TestSuite(l)
983 # vim: set filetype=python ts=4 sw=4 et si