2435134a76620c1a22b014bf364e1771e4fe0834
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.9 2003-11-16 19:59:06 jlgijsbers Exp $
20 import unittest, os, shutil, errno, imp, sys, time, pprint
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 import init
26 from roundup.indexer import Indexer
28 def setupSchema(db, create, module):
29 status = module.Class(db, "status", name=String())
30 status.setkey("name")
31 user = module.Class(db, "user", username=String(), password=Password(),
32 assignable=Boolean(), age=Number(), roles=String())
33 user.setkey("username")
34 file = module.FileClass(db, "file", name=String(), type=String(),
35 comment=String(indexme="yes"), fooz=Password())
36 issue = module.IssueClass(db, "issue", title=String(indexme="yes"),
37 status=Link("status"), nosy=Multilink("user"), deadline=Date(),
38 foo=Interval(), files=Multilink("file"), assignedto=Link('user'))
39 session = module.Class(db, 'session', title=String())
40 session.disableJournalling()
41 db.post_init()
42 if create:
43 user.create(username="admin", roles='Admin')
44 status.create(name="unread")
45 status.create(name="in-progress")
46 status.create(name="testing")
47 status.create(name="resolved")
48 db.commit()
50 class MyTestCase(unittest.TestCase):
51 def tearDown(self):
52 if hasattr(self, 'db'):
53 self.db.close()
54 if os.path.exists('_test_dir'):
55 shutil.rmtree('_test_dir')
57 class config:
58 DATABASE='_test_dir'
59 MAILHOST = 'localhost'
60 MAIL_DOMAIN = 'fill.me.in.'
61 TRACKER_NAME = 'Roundup issue tracker'
62 TRACKER_EMAIL = 'issue_tracker@%s'%MAIL_DOMAIN
63 TRACKER_WEB = 'http://some.useful.url/'
64 ADMIN_EMAIL = 'roundup-admin@%s'%MAIL_DOMAIN
65 FILTER_POSITION = 'bottom' # one of 'top', 'bottom', 'top and bottom'
66 ANONYMOUS_ACCESS = 'deny' # either 'deny' or 'allow'
67 ANONYMOUS_REGISTER = 'deny' # either 'deny' or 'allow'
68 MESSAGES_TO_AUTHOR = 'no' # either 'yes' or 'no'
69 EMAIL_SIGNATURE_POSITION = 'bottom'
72 class DBTest(MyTestCase):
73 def setUp(self):
74 # remove previous test, ignore errors
75 if os.path.exists(config.DATABASE):
76 shutil.rmtree(config.DATABASE)
77 os.makedirs(config.DATABASE + '/files')
78 self.db = self.module.Database(config, 'admin')
79 setupSchema(self.db, 1, self.module)
81 #
82 # schema mutation
83 #
84 def testAddProperty(self):
85 self.db.issue.create(title="spam", status='1')
86 self.db.commit()
88 self.db.issue.addprop(fixer=Link("user"))
89 # force any post-init stuff to happen
90 self.db.post_init()
91 props = self.db.issue.getprops()
92 keys = props.keys()
93 keys.sort()
94 self.assertEqual(keys, ['activity', 'assignedto', 'creation',
95 'creator', 'deadline', 'files', 'fixer', 'foo', 'id', 'messages',
96 'nosy', 'status', 'superseder', 'title'])
97 self.assertEqual(self.db.issue.get('1', "fixer"), None)
99 def testRemoveProperty(self):
100 self.db.issue.create(title="spam", status='1')
101 self.db.commit()
103 del self.db.issue.properties['title']
104 self.db.post_init()
105 props = self.db.issue.getprops()
106 keys = props.keys()
107 keys.sort()
108 self.assertEqual(keys, ['activity', 'assignedto', 'creation',
109 'creator', 'deadline', 'files', 'foo', 'id', 'messages',
110 'nosy', 'status', 'superseder'])
111 self.assertEqual(self.db.issue.list(), ['1'])
113 def testAddRemoveProperty(self):
114 self.db.issue.create(title="spam", status='1')
115 self.db.commit()
117 self.db.issue.addprop(fixer=Link("user"))
118 del self.db.issue.properties['title']
119 self.db.post_init()
120 props = self.db.issue.getprops()
121 keys = props.keys()
122 keys.sort()
123 self.assertEqual(keys, ['activity', 'assignedto', 'creation',
124 'creator', 'deadline', 'files', 'fixer', 'foo', 'id', 'messages',
125 'nosy', 'status', 'superseder'])
126 self.assertEqual(self.db.issue.list(), ['1'])
128 #
129 # basic operations
130 #
131 def testIDGeneration(self):
132 id1 = self.db.issue.create(title="spam", status='1')
133 id2 = self.db.issue.create(title="eggs", status='2')
134 self.assertNotEqual(id1, id2)
136 def testStringChange(self):
137 for commit in (0,1):
138 # test set & retrieve
139 nid = self.db.issue.create(title="spam", status='1')
140 self.assertEqual(self.db.issue.get(nid, 'title'), 'spam')
142 # change and make sure we retrieve the correct value
143 self.db.issue.set(nid, title='eggs')
144 if commit: self.db.commit()
145 self.assertEqual(self.db.issue.get(nid, 'title'), 'eggs')
147 def testStringUnset(self):
148 for commit in (0,1):
149 nid = self.db.issue.create(title="spam", status='1')
150 if commit: self.db.commit()
151 self.assertEqual(self.db.issue.get(nid, 'title'), 'spam')
152 # make sure we can unset
153 self.db.issue.set(nid, title=None)
154 if commit: self.db.commit()
155 self.assertEqual(self.db.issue.get(nid, "title"), None)
157 def testLinkChange(self):
158 for commit in (0,1):
159 nid = self.db.issue.create(title="spam", status='1')
160 if commit: self.db.commit()
161 self.assertEqual(self.db.issue.get(nid, "status"), '1')
162 self.db.issue.set(nid, status='2')
163 if commit: self.db.commit()
164 self.assertEqual(self.db.issue.get(nid, "status"), '2')
166 def testLinkUnset(self):
167 for commit in (0,1):
168 nid = self.db.issue.create(title="spam", status='1')
169 if commit: self.db.commit()
170 self.db.issue.set(nid, status=None)
171 if commit: self.db.commit()
172 self.assertEqual(self.db.issue.get(nid, "status"), None)
174 def testMultilinkChange(self):
175 for commit in (0,1):
176 u1 = self.db.user.create(username='foo%s'%commit)
177 u2 = self.db.user.create(username='bar%s'%commit)
178 nid = self.db.issue.create(title="spam", nosy=[u1])
179 if commit: self.db.commit()
180 self.assertEqual(self.db.issue.get(nid, "nosy"), [u1])
181 self.db.issue.set(nid, nosy=[])
182 if commit: self.db.commit()
183 self.assertEqual(self.db.issue.get(nid, "nosy"), [])
184 self.db.issue.set(nid, nosy=[u1,u2])
185 if commit: self.db.commit()
186 self.assertEqual(self.db.issue.get(nid, "nosy"), [u1,u2])
188 def testDateChange(self):
189 for commit in (0,1):
190 nid = self.db.issue.create(title="spam", status='1')
191 a = self.db.issue.get(nid, "deadline")
192 if commit: self.db.commit()
193 self.db.issue.set(nid, deadline=date.Date())
194 b = self.db.issue.get(nid, "deadline")
195 if commit: self.db.commit()
196 self.assertNotEqual(a, b)
197 self.assertNotEqual(b, date.Date('1970-1-1 00:00:00'))
199 def testDateUnset(self):
200 for commit in (0,1):
201 nid = self.db.issue.create(title="spam", status='1')
202 self.db.issue.set(nid, deadline=date.Date())
203 if commit: self.db.commit()
204 self.assertNotEqual(self.db.issue.get(nid, "deadline"), None)
205 self.db.issue.set(nid, deadline=None)
206 if commit: self.db.commit()
207 self.assertEqual(self.db.issue.get(nid, "deadline"), None)
209 def testIntervalChange(self):
210 for commit in (0,1):
211 nid = self.db.issue.create(title="spam", status='1')
212 if commit: self.db.commit()
213 a = self.db.issue.get(nid, "foo")
214 i = date.Interval('-1d')
215 self.db.issue.set(nid, foo=i)
216 if commit: self.db.commit()
217 self.assertNotEqual(self.db.issue.get(nid, "foo"), a)
218 self.assertEqual(i, self.db.issue.get(nid, "foo"))
219 j = date.Interval('1y')
220 self.db.issue.set(nid, foo=j)
221 if commit: self.db.commit()
222 self.assertNotEqual(self.db.issue.get(nid, "foo"), i)
223 self.assertEqual(j, self.db.issue.get(nid, "foo"))
225 def testIntervalUnset(self):
226 for commit in (0,1):
227 nid = self.db.issue.create(title="spam", status='1')
228 self.db.issue.set(nid, foo=date.Interval('-1d'))
229 if commit: self.db.commit()
230 self.assertNotEqual(self.db.issue.get(nid, "foo"), None)
231 self.db.issue.set(nid, foo=None)
232 if commit: self.db.commit()
233 self.assertEqual(self.db.issue.get(nid, "foo"), None)
235 def testBooleanChange(self):
236 userid = self.db.user.create(username='foo', assignable=1)
237 self.assertEqual(1, self.db.user.get(userid, 'assignable'))
238 self.db.user.set(userid, assignable=0)
239 self.assertEqual(self.db.user.get(userid, 'assignable'), 0)
241 def testBooleanUnset(self):
242 nid = self.db.user.create(username='foo', assignable=1)
243 self.db.user.set(nid, assignable=None)
244 self.assertEqual(self.db.user.get(nid, "assignable"), None)
246 def testNumberChange(self):
247 nid = self.db.user.create(username='foo', age=1)
248 self.assertEqual(1, self.db.user.get(nid, 'age'))
249 self.db.user.set(nid, age=3)
250 self.assertNotEqual(self.db.user.get(nid, 'age'), 1)
251 self.db.user.set(nid, age=1.0)
252 self.assertEqual(self.db.user.get(nid, 'age'), 1)
253 self.db.user.set(nid, age=0)
254 self.assertEqual(self.db.user.get(nid, 'age'), 0)
256 nid = self.db.user.create(username='bar', age=0)
257 self.assertEqual(self.db.user.get(nid, 'age'), 0)
259 def testNumberUnset(self):
260 nid = self.db.user.create(username='foo', age=1)
261 self.db.user.set(nid, age=None)
262 self.assertEqual(self.db.user.get(nid, "age"), None)
264 def testKeyValue(self):
265 newid = self.db.user.create(username="spam")
266 self.assertEqual(self.db.user.lookup('spam'), newid)
267 self.db.commit()
268 self.assertEqual(self.db.user.lookup('spam'), newid)
269 self.db.user.retire(newid)
270 self.assertRaises(KeyError, self.db.user.lookup, 'spam')
272 # use the key again now that the old is retired
273 newid2 = self.db.user.create(username="spam")
274 self.assertNotEqual(newid, newid2)
275 # try to restore old node. this shouldn't succeed!
276 self.assertRaises(KeyError, self.db.user.restore, newid)
278 def testRetire(self):
279 self.db.issue.create(title="spam", status='1')
280 b = self.db.status.get('1', 'name')
281 a = self.db.status.list()
282 self.db.status.retire('1')
283 # make sure the list is different
284 self.assertNotEqual(a, self.db.status.list())
285 # can still access the node if necessary
286 self.assertEqual(self.db.status.get('1', 'name'), b)
287 self.db.commit()
288 self.assertEqual(self.db.status.get('1', 'name'), b)
289 self.assertNotEqual(a, self.db.status.list())
290 # try to restore retired node
291 self.db.status.restore('1')
293 def testCacheCreateSet(self):
294 self.db.issue.create(title="spam", status='1')
295 a = self.db.issue.get('1', 'title')
296 self.assertEqual(a, 'spam')
297 self.db.issue.set('1', title='ham')
298 b = self.db.issue.get('1', 'title')
299 self.assertEqual(b, 'ham')
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)
618 got = self.db.issue.find(status={'1':1})
619 got.sort()
620 self.assertEqual(got, ids)
622 # none
623 self.assertEqual(self.db.issue.find(status='4'), [])
624 self.assertEqual(self.db.issue.find(status={'4':1}), [])
626 # should match first and third
627 got = self.db.issue.find(assignedto=None)
628 got.sort()
629 self.assertEqual(got, ids)
630 got = self.db.issue.find(assignedto={None:1})
631 got.sort()
632 self.assertEqual(got, ids)
634 # should match first three
635 got = self.db.issue.find(status='1', nosy='2')
636 got.sort()
637 ids.append(oddid)
638 ids.sort()
639 self.assertEqual(got, ids)
640 got = self.db.issue.find(status={'1':1}, nosy={'2':1})
641 got.sort()
642 self.assertEqual(got, ids)
644 # none
645 self.assertEqual(self.db.issue.find(status='4', nosy='3'), [])
646 self.assertEqual(self.db.issue.find(status={'4':1}, nosy={'3':1}), [])
648 def testStringFind(self):
649 ids = []
650 ids.append(self.db.issue.create(title="spam"))
651 self.db.issue.create(title="not spam")
652 ids.append(self.db.issue.create(title="spam"))
653 ids.sort()
654 got = self.db.issue.stringFind(title='spam')
655 got.sort()
656 self.assertEqual(got, ids)
657 self.assertEqual(self.db.issue.stringFind(title='fubar'), [])
659 def filteringSetup(self):
660 for user in (
661 {'username': 'bleep'},
662 {'username': 'blop'},
663 {'username': 'blorp'}):
664 self.db.user.create(**user)
665 iss = self.db.issue
666 for issue in (
667 {'title': 'issue one', 'status': '2',
668 'foo': date.Interval('1:10'),
669 'deadline': date.Date('2003-01-01.00:00')},
670 {'title': 'issue two', 'status': '1',
671 'foo': date.Interval('1d'),
672 'deadline': date.Date('2003-02-16.22:50')},
673 {'title': 'issue three', 'status': '1',
674 'nosy': ['1','2'], 'deadline': date.Date('2003-02-18')},
675 {'title': 'non four', 'status': '3',
676 'foo': date.Interval('0:10'),
677 'nosy': ['1'], 'deadline': date.Date('2004-03-08')}):
678 self.db.issue.create(**issue)
679 self.db.commit()
680 return self.assertEqual, self.db.issue.filter
682 def testFilteringID(self):
683 ae, filt = self.filteringSetup()
684 ae(filt(None, {'id': '1'}, ('+','id'), (None,None)), ['1'])
686 def testFilteringString(self):
687 ae, filt = self.filteringSetup()
688 ae(filt(None, {'title': ['one']}, ('+','id'), (None,None)), ['1'])
689 ae(filt(None, {'title': ['issue']}, ('+','id'), (None,None)),
690 ['1','2','3'])
691 ae(filt(None, {'title': ['one', 'two']}, ('+','id'), (None,None)),
692 ['1', '2'])
694 def testFilteringLink(self):
695 ae, filt = self.filteringSetup()
696 ae(filt(None, {'status': '1'}, ('+','id'), (None,None)), ['2','3'])
698 def testFilteringRetired(self):
699 ae, filt = self.filteringSetup()
700 self.db.issue.retire('2')
701 ae(filt(None, {'status': '1'}, ('+','id'), (None,None)), ['3'])
703 def testFilteringMultilink(self):
704 ae, filt = self.filteringSetup()
705 ae(filt(None, {'nosy': '2'}, ('+','id'), (None,None)), ['3'])
706 ae(filt(None, {'nosy': '-1'}, ('+','id'), (None,None)), ['1', '2'])
708 def testFilteringMany(self):
709 ae, filt = self.filteringSetup()
710 ae(filt(None, {'nosy': '2', 'status': '1'}, ('+','id'), (None,None)),
711 ['3'])
713 def testFilteringRange(self):
714 ae, filt = self.filteringSetup()
715 # Date ranges
716 ae(filt(None, {'deadline': 'from 2003-02-10 to 2003-02-23'}), ['2','3'])
717 ae(filt(None, {'deadline': '2003-02-10; 2003-02-23'}), ['2','3'])
718 ae(filt(None, {'deadline': '; 2003-02-16'}), ['1'])
719 # Lets assume people won't invent a time machine, otherwise this test
720 # may fail :)
721 ae(filt(None, {'deadline': 'from 2003-02-16'}), ['2', '3', '4'])
722 ae(filt(None, {'deadline': '2003-02-16;'}), ['2', '3', '4'])
723 # year and month granularity
724 ae(filt(None, {'deadline': '2002'}), [])
725 ae(filt(None, {'deadline': '2003'}), ['1', '2', '3'])
726 ae(filt(None, {'deadline': '2004'}), ['4'])
727 ae(filt(None, {'deadline': '2003-02'}), ['2', '3'])
728 ae(filt(None, {'deadline': '2003-03'}), [])
729 ae(filt(None, {'deadline': '2003-02-16'}), ['2'])
730 ae(filt(None, {'deadline': '2003-02-17'}), [])
731 # Interval ranges
732 ae(filt(None, {'foo': 'from 0:50 to 2:00'}), ['1'])
733 ae(filt(None, {'foo': 'from 0:50 to 1d 2:00'}), ['1', '2'])
734 ae(filt(None, {'foo': 'from 5:50'}), ['2'])
735 ae(filt(None, {'foo': 'to 0:05'}), [])
737 def testFilteringIntervalSort(self):
738 ae, filt = self.filteringSetup()
739 # ascending should sort None, 1:10, 1d
740 ae(filt(None, {}, ('+','foo'), (None,None)), ['3', '4', '1', '2'])
741 # descending should sort 1d, 1:10, None
742 ae(filt(None, {}, ('-','foo'), (None,None)), ['2', '1', '4', '3'])
744 # XXX add sorting tests for other types
745 # XXX test auditors and reactors
747 def testImportExport(self):
748 # use the filtering setup to create a bunch of items
749 ae, filt = self.filteringSetup()
750 self.db.user.retire('3')
751 self.db.issue.retire('2')
753 # grab snapshot of the current database
754 orig = {}
755 for cn,klass in self.db.classes.items():
756 cl = orig[cn] = {}
757 for id in klass.list():
758 it = cl[id] = {}
759 for name in klass.getprops().keys():
760 it[name] = klass.get(id, name)
762 # grab the export
763 export = {}
764 for cn,klass in self.db.classes.items():
765 names = klass.getprops().keys()
766 cl = export[cn] = [names+['is retired']]
767 for id in klass.getnodeids():
768 cl.append(klass.export_list(names, id))
770 # shut down this db and nuke it
771 self.db.close()
772 self.nuke_database()
774 # open a new, empty database
775 os.makedirs(config.DATABASE + '/files')
776 self.db = self.module.Database(config, 'admin')
777 setupSchema(self.db, 0, self.module)
779 # import
780 for cn, items in export.items():
781 klass = self.db.classes[cn]
782 names = items[0]
783 for itemprops in items[1:]:
784 klass.import_list(names, itemprops)
786 # grab snapshot of the current database
787 for cn, items in orig.items():
788 klass = self.db.classes[cn]
789 # ensure retired items are retired :)
790 l = items.keys(); l.sort()
791 m = klass.list(); m.sort()
792 ae(l, m)
793 for id, props in items.items():
794 for name, value in props.items():
795 ae(klass.get(id, name), value)
797 # make sure the retired items are actually imported
798 ae(self.db.user.get('3', 'username'), 'blop')
799 ae(self.db.issue.get('2', 'title'), 'issue two')
801 def testSafeGet(self):
802 # existent nodeid, existent property
803 self.assertEqual(self.db.user.safeget('1', 'username'), 'admin')
804 # existent nodeid, nonexistent property
805 self.assertEqual(self.db.user.safeget('1', 'nonexistent'), None)
806 # nonexistent nodeid, existent property
807 self.assertEqual(self.db.user.safeget('999', 'username'), None)
808 # nonexistent nodeid, nonexistent property
809 self.assertEqual(self.db.user.safeget('999', 'nonexistent'), None)
810 # different default
811 self.assertEqual(self.db.issue.safeget('999', 'nosy', []), [])
813 class ROTest(MyTestCase):
814 def setUp(self):
815 # remove previous test, ignore errors
816 if os.path.exists(config.DATABASE):
817 shutil.rmtree(config.DATABASE)
818 os.makedirs(config.DATABASE + '/files')
819 self.db = self.module.Database(config, 'admin')
820 setupSchema(self.db, 1, self.module)
821 self.db.close()
823 self.db = self.module.Database(config)
824 setupSchema(self.db, 0, self.module)
826 def testExceptions(self):
827 # this tests the exceptions that should be raised
828 ar = self.assertRaises
830 # this tests the exceptions that should be raised
831 ar(DatabaseError, self.db.status.create, name="foo")
832 ar(DatabaseError, self.db.status.set, '1', name="foo")
833 ar(DatabaseError, self.db.status.retire, '1')
836 class SchemaTest(MyTestCase):
837 def setUp(self):
838 # remove previous test, ignore errors
839 if os.path.exists(config.DATABASE):
840 shutil.rmtree(config.DATABASE)
841 os.makedirs(config.DATABASE + '/files')
843 def init_a(self):
844 self.db = self.module.Database(config, 'admin')
845 a = self.module.Class(self.db, "a", name=String())
846 a.setkey("name")
847 self.db.post_init()
849 def init_ab(self):
850 self.db = self.module.Database(config, 'admin')
851 a = self.module.Class(self.db, "a", name=String())
852 a.setkey("name")
853 b = self.module.Class(self.db, "b", name=String())
854 b.setkey("name")
855 self.db.post_init()
857 def test_addNewClass(self):
858 self.init_a()
859 aid = self.db.a.create(name='apple')
860 self.db.commit(); self.db.close()
862 # add a new class to the schema and check creation of new items
863 # (and existence of old ones)
864 self.init_ab()
865 bid = self.db.b.create(name='bear')
866 self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
867 self.db.commit()
868 self.db.close()
870 # now check we can recall the added class' items
871 self.init_ab()
872 self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
873 self.assertEqual(self.db.a.lookup('apple'), aid)
874 self.assertEqual(self.db.b.get(bid, 'name'), 'bear')
875 self.assertEqual(self.db.b.lookup('bear'), bid)
877 # confirm journal's ok
878 self.db.getjournal('a', aid)
879 self.db.getjournal('b', bid)
881 def init_amod(self):
882 self.db = self.module.Database(config, 'admin')
883 a = self.module.Class(self.db, "a", name=String(), fooz=String())
884 a.setkey("name")
885 b = self.module.Class(self.db, "b", name=String())
886 b.setkey("name")
887 self.db.post_init()
889 def test_modifyClass(self):
890 self.init_ab()
892 # add item to user and issue class
893 aid = self.db.a.create(name='apple')
894 bid = self.db.b.create(name='bear')
895 self.db.commit(); self.db.close()
897 # modify "a" schema
898 self.init_amod()
899 self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
900 self.assertEqual(self.db.a.get(aid, 'fooz'), None)
901 self.assertEqual(self.db.b.get(aid, 'name'), 'bear')
902 aid2 = self.db.a.create(name='aardvark', fooz='booz')
903 self.db.commit(); self.db.close()
905 # test
906 self.init_amod()
907 self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
908 self.assertEqual(self.db.a.get(aid, 'fooz'), None)
909 self.assertEqual(self.db.b.get(aid, 'name'), 'bear')
910 self.assertEqual(self.db.a.get(aid2, 'name'), 'aardvark')
911 self.assertEqual(self.db.a.get(aid2, 'fooz'), 'booz')
913 # confirm journal's ok
914 self.db.getjournal('a', aid)
915 self.db.getjournal('a', aid2)
917 def init_amodkey(self):
918 self.db = self.module.Database(config, 'admin')
919 a = self.module.Class(self.db, "a", name=String(), fooz=String())
920 a.setkey("fooz")
921 b = self.module.Class(self.db, "b", name=String())
922 b.setkey("name")
923 self.db.post_init()
925 def test_changeClassKey(self):
926 self.init_amod()
927 aid = self.db.a.create(name='apple')
928 self.assertEqual(self.db.a.lookup('apple'), aid)
929 self.db.commit(); self.db.close()
931 # change the key to fooz on a
932 self.init_amodkey()
933 self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
934 self.assertEqual(self.db.a.get(aid, 'fooz'), None)
935 self.assertRaises(KeyError, self.db.a.lookup, 'apple')
936 aid2 = self.db.a.create(name='aardvark', fooz='booz')
937 self.db.commit(); self.db.close()
939 # check
940 self.init_amodkey()
941 self.assertEqual(self.db.a.lookup('booz'), aid2)
943 # confirm journal's ok
944 self.db.getjournal('a', aid)
946 def init_ml(self):
947 self.db = self.module.Database(config, 'admin')
948 a = self.module.Class(self.db, "a", name=String())
949 a.setkey('name')
950 b = self.module.Class(self.db, "b", name=String(),
951 fooz=Multilink('a'))
952 b.setkey("name")
953 self.db.post_init()
955 def test_makeNewMultilink(self):
956 self.init_a()
957 aid = self.db.a.create(name='apple')
958 self.assertEqual(self.db.a.lookup('apple'), aid)
959 self.db.commit(); self.db.close()
961 # add a multilink prop
962 self.init_ml()
963 bid = self.db.b.create(name='bear', fooz=[aid])
964 self.assertEqual(self.db.b.find(fooz=aid), [bid])
965 self.assertEqual(self.db.a.lookup('apple'), aid)
966 self.db.commit(); self.db.close()
968 # check
969 self.init_ml()
970 self.assertEqual(self.db.b.find(fooz=aid), [bid])
971 self.assertEqual(self.db.a.lookup('apple'), aid)
972 self.assertEqual(self.db.b.lookup('bear'), bid)
974 # confirm journal's ok
975 self.db.getjournal('a', aid)
976 self.db.getjournal('b', bid)
978 def test_removeMultilink(self):
979 # add a multilink prop
980 self.init_ml()
981 aid = self.db.a.create(name='apple')
982 bid = self.db.b.create(name='bear', fooz=[aid])
983 self.assertEqual(self.db.b.find(fooz=aid), [bid])
984 self.assertEqual(self.db.a.lookup('apple'), aid)
985 self.assertEqual(self.db.b.lookup('bear'), bid)
986 self.db.commit(); self.db.close()
988 # remove the multilink
989 self.init_ab()
990 self.assertEqual(self.db.a.lookup('apple'), aid)
991 self.assertEqual(self.db.b.lookup('bear'), bid)
993 # confirm journal's ok
994 self.db.getjournal('a', aid)
995 self.db.getjournal('b', bid)
997 def test_removeClass(self):
998 self.init_ml()
999 aid = self.db.a.create(name='apple')
1000 bid = self.db.b.create(name='bear', fooz=[aid])
1001 self.db.commit(); self.db.close()
1003 # drop the b class
1004 self.init_a()
1005 self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
1006 self.assertEqual(self.db.a.lookup('apple'), aid)
1007 self.db.commit(); self.db.close()
1009 # now check we can recall the added class' items
1010 self.init_a()
1011 self.assertEqual(self.db.a.get(aid, 'name'), 'apple')
1012 self.assertEqual(self.db.a.lookup('apple'), aid)
1014 # confirm journal's ok
1015 self.db.getjournal('a', aid)
1018 class ClassicInitTest(unittest.TestCase):
1019 count = 0
1020 db = None
1021 extra_config = ''
1023 def setUp(self):
1024 ClassicInitTest.count = ClassicInitTest.count + 1
1025 self.dirname = '_test_init_%s'%self.count
1026 try:
1027 shutil.rmtree(self.dirname)
1028 except OSError, error:
1029 if error.errno not in (errno.ENOENT, errno.ESRCH): raise
1031 def testCreation(self):
1032 ae = self.assertEqual
1034 # create the instance
1035 init.install(self.dirname, 'templates/classic')
1036 init.write_select_db(self.dirname, self.backend)
1038 if self.extra_config:
1039 f = open(os.path.join(self.dirname, 'config.py'), 'a')
1040 try:
1041 f.write(self.extra_config)
1042 finally:
1043 f.close()
1045 init.initialise(self.dirname, 'sekrit')
1047 # check we can load the package
1048 instance = imp.load_package(self.dirname, self.dirname)
1050 # and open the database
1051 db = self.db = instance.open()
1053 # check the basics of the schema and initial data set
1054 l = db.priority.list()
1055 ae(l, ['1', '2', '3', '4', '5'])
1056 l = db.status.list()
1057 ae(l, ['1', '2', '3', '4', '5', '6', '7', '8'])
1058 l = db.keyword.list()
1059 ae(l, [])
1060 l = db.user.list()
1061 ae(l, ['1', '2'])
1062 l = db.msg.list()
1063 ae(l, [])
1064 l = db.file.list()
1065 ae(l, [])
1066 l = db.issue.list()
1067 ae(l, [])
1069 def tearDown(self):
1070 if self.db is not None:
1071 self.db.close()
1072 try:
1073 shutil.rmtree(self.dirname)
1074 except OSError, error:
1075 if error.errno not in (errno.ENOENT, errno.ESRCH): raise