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