1"""
2Copyright (c) 2008-2020, Jesus Cea Avion <jcea@jcea.es>
3All rights reserved.
4
5Redistribution and use in source and binary forms, with or without
6modification, are permitted provided that the following conditions
7are met:
8
9    1. Redistributions of source code must retain the above copyright
10    notice, this list of conditions and the following disclaimer.
11
12    2. Redistributions in binary form must reproduce the above
13    copyright notice, this list of conditions and the following
14    disclaimer in the documentation and/or other materials provided
15    with the distribution.
16
17    3. Neither the name of Jesus Cea Avion nor the names of its
18    contributors may be used to endorse or promote products derived
19    from this software without specific prior written permission.
20
21    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
22    CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
23    INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
24    MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
25    DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
26    BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
27    EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
28            TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
29            DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
30    ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
31    TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
32    THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
33    SUCH DAMAGE.
34    """
35
36"""
37TestCases for DB.associate.
38"""
39
40import sys, os, string
41import time
42from pprint import pprint
43
44import unittest
45from .test_all import db, dbshelve, test_support, verbose, have_threads, \
46        get_new_environment_path
47
48
49#----------------------------------------------------------------------
50
51
52musicdata = {
531 : ("Bad English", "The Price Of Love", "Rock"),
542 : ("DNA featuring Suzanne Vega", "Tom's Diner", "Rock"),
553 : ("George Michael", "Praying For Time", "Rock"),
564 : ("Gloria Estefan", "Here We Are", "Rock"),
575 : ("Linda Ronstadt", "Don't Know Much", "Rock"),
586 : ("Michael Bolton", "How Am I Supposed To Live Without You", "Blues"),
597 : ("Paul Young", "Oh Girl", "Rock"),
608 : ("Paula Abdul", "Opposites Attract", "Rock"),
619 : ("Richard Marx", "Should've Known Better", "Rock"),
6210: ("Rod Stewart", "Forever Young", "Rock"),
6311: ("Roxette", "Dangerous", "Rock"),
6412: ("Sheena Easton", "The Lover In Me", "Rock"),
6513: ("Sinead O'Connor", "Nothing Compares 2 U", "Rock"),
6614: ("Stevie B.", "Because I Love You", "Rock"),
6715: ("Taylor Dayne", "Love Will Lead You Back", "Rock"),
6816: ("The Bangles", "Eternal Flame", "Rock"),
6917: ("Wilson Phillips", "Release Me", "Rock"),
7018: ("Billy Joel", "Blonde Over Blue", "Rock"),
7119: ("Billy Joel", "Famous Last Words", "Rock"),
7220: ("Billy Joel", "Lullabye (Goodnight, My Angel)", "Rock"),
7321: ("Billy Joel", "The River Of Dreams", "Rock"),
7422: ("Billy Joel", "Two Thousand Years", "Rock"),
7523: ("Janet Jackson", "Alright", "Rock"),
7624: ("Janet Jackson", "Black Cat", "Rock"),
7725: ("Janet Jackson", "Come Back To Me", "Rock"),
7826: ("Janet Jackson", "Escapade", "Rock"),
7927: ("Janet Jackson", "Love Will Never Do (Without You)", "Rock"),
8028: ("Janet Jackson", "Miss You Much", "Rock"),
8129: ("Janet Jackson", "Rhythm Nation", "Rock"),
8230: ("Janet Jackson", "State Of The World", "Rock"),
8331: ("Janet Jackson", "The Knowledge", "Rock"),
8432: ("Spyro Gyra", "End of Romanticism", "Jazz"),
8533: ("Spyro Gyra", "Heliopolis", "Jazz"),
8634: ("Spyro Gyra", "Jubilee", "Jazz"),
8735: ("Spyro Gyra", "Little Linda", "Jazz"),
8836: ("Spyro Gyra", "Morning Dance", "Jazz"),
8937: ("Spyro Gyra", "Song for Lorraine", "Jazz"),
9038: ("Yes", "Owner Of A Lonely Heart", "Rock"),
9139: ("Yes", "Rhythm Of Love", "Rock"),
9240: ("Cusco", "Dream Catcher", "New Age"),
9341: ("Cusco", "Geronimos Laughter", "New Age"),
9442: ("Cusco", "Ghost Dance", "New Age"),
9543: ("Blue Man Group", "Drumbone", "New Age"),
9644: ("Blue Man Group", "Endless Column", "New Age"),
9745: ("Blue Man Group", "Klein Mandelbrot", "New Age"),
9846: ("Kenny G", "Silhouette", "Jazz"),
9947: ("Sade", "Smooth Operator", "Jazz"),
10048: ("David Arkenstone", "Papillon (On The Wings Of The Butterfly)",
101     "New Age"),
10249: ("David Arkenstone", "Stepping Stars", "New Age"),
10350: ("David Arkenstone", "Carnation Lily Lily Rose", "New Age"),
10451: ("David Lanz", "Behind The Waterfall", "New Age"),
10552: ("David Lanz", "Cristofori's Dream", "New Age"),
10653: ("David Lanz", "Heartsounds", "New Age"),
10754: ("David Lanz", "Leaves on the Seine", "New Age"),
10899: ("unknown artist", "Unnamed song", "Unknown"),
109}
110
111#----------------------------------------------------------------------
112
113class AssociateErrorTestCase(unittest.TestCase):
114    def setUp(self):
115        self.filename = self.__class__.__name__ + '.db'
116        self.homeDir = get_new_environment_path()
117        self.env = db.DBEnv()
118        self.env.open(self.homeDir, db.DB_CREATE | db.DB_INIT_MPOOL)
119
120    def tearDown(self):
121        self.env.close()
122        self.env = None
123        test_support.rmtree(self.homeDir)
124
125    def test00_associateDBError(self):
126        if verbose:
127            print('\n', '-=' * 30)
128            print("Running %s.test00_associateDBError..." % \
129                  self.__class__.__name__)
130
131        dupDB = db.DB(self.env)
132        dupDB.set_flags(db.DB_DUP)
133        dupDB.open(self.filename, "primary", db.DB_BTREE, db.DB_CREATE)
134
135        secDB = db.DB(self.env)
136        secDB.open(self.filename, "secondary", db.DB_BTREE, db.DB_CREATE)
137
138        # dupDB has been configured to allow duplicates, it can't
139        # associate with a secondary.  Berkeley DB will return an error.
140        try:
141            def f(a,b): return a+b
142            dupDB.associate(secDB, f)
143        except db.DBError:
144            # good
145            secDB.close()
146            dupDB.close()
147        else:
148            secDB.close()
149            dupDB.close()
150            self.fail("DBError exception was expected")
151
152
153
154#----------------------------------------------------------------------
155
156
157class AssociateTestCase(unittest.TestCase):
158    keytype = ''
159    envFlags = 0
160    dbFlags = 0
161
162    def setUp(self):
163        self.filename = self.__class__.__name__ + '.db'
164        self.homeDir = get_new_environment_path()
165        self.env = db.DBEnv()
166        self.env.open(self.homeDir, db.DB_CREATE | db.DB_INIT_MPOOL |
167                               db.DB_INIT_LOCK | db.DB_THREAD | self.envFlags)
168
169    def tearDown(self):
170        self.closeDB()
171        self.env.close()
172        self.env = None
173        test_support.rmtree(self.homeDir)
174
175    def addDataToDB(self, d, txn=None):
176        for key, value in list(musicdata.items()):
177            if type(self.keytype) == type(''):
178                key = "%02d" % key
179            d.put(key, '|'.join(value), txn=txn)
180
181    def createDB(self, txn=None):
182        self.cur = None
183        self.secDB = None
184        self.primary = db.DB(self.env)
185        self.primary.set_get_returns_none(2)
186        self.primary.open(self.filename, "primary", self.dbtype,
187                      db.DB_CREATE | db.DB_THREAD | self.dbFlags, txn=txn)
188
189    def closeDB(self):
190        if self.cur:
191            self.cur.close()
192            self.cur = None
193        if self.secDB:
194            self.secDB.close()
195            self.secDB = None
196        self.primary.close()
197        self.primary = None
198
199    def getDB(self):
200        return self.primary
201
202
203    def _associateWithDB(self, getGenre):
204        self.createDB()
205
206        self.secDB = db.DB(self.env)
207        self.secDB.set_flags(db.DB_DUP)
208        self.secDB.set_get_returns_none(2)
209        self.secDB.open(self.filename, "secondary", db.DB_BTREE,
210                   db.DB_CREATE | db.DB_THREAD | self.dbFlags)
211        self.getDB().associate(self.secDB, getGenre)
212
213        self.addDataToDB(self.getDB())
214
215        self.finish_test(self.secDB)
216
217    def test01_associateWithDB(self):
218        if verbose:
219            print('\n', '-=' * 30)
220            print("Running %s.test01_associateWithDB..." % \
221                  self.__class__.__name__)
222
223        return self._associateWithDB(self.getGenre)
224
225    def _associateAfterDB(self, getGenre) :
226        self.createDB()
227        self.addDataToDB(self.getDB())
228
229        self.secDB = db.DB(self.env)
230        self.secDB.set_flags(db.DB_DUP)
231        self.secDB.open(self.filename, "secondary", db.DB_BTREE,
232                   db.DB_CREATE | db.DB_THREAD | self.dbFlags)
233
234        # adding the DB_CREATE flag will cause it to index existing records
235        self.getDB().associate(self.secDB, getGenre, db.DB_CREATE)
236
237        self.finish_test(self.secDB)
238
239    def test02_associateAfterDB(self):
240        if verbose:
241            print('\n', '-=' * 30)
242            print("Running %s.test02_associateAfterDB..." % \
243                  self.__class__.__name__)
244
245        return self._associateAfterDB(self.getGenre)
246
247    def test03_associateWithDB(self):
248        if verbose:
249            print('\n', '-=' * 30)
250            print("Running %s.test03_associateWithDB..." % \
251                  self.__class__.__name__)
252
253        return self._associateWithDB(self.getGenreList)
254
255    def test04_associateAfterDB(self):
256        if verbose:
257            print('\n', '-=' * 30)
258            print("Running %s.test04_associateAfterDB..." % \
259                  self.__class__.__name__)
260
261        return self._associateAfterDB(self.getGenreList)
262
263
264    def finish_test(self, secDB, txn=None):
265        # 'Blues' should not be in the secondary database
266        vals = secDB.pget('Blues', txn=txn)
267        self.assertEqual(vals, None, vals)
268
269        vals = secDB.pget('Unknown', txn=txn)
270        self.assertTrue(vals[0] == 99 or vals[0] == '99', vals)
271        vals[1].index('Unknown')
272        vals[1].index('Unnamed')
273        vals[1].index('unknown')
274
275        if verbose:
276            print("Primary key traversal:")
277        self.cur = self.getDB().cursor(txn)
278        count = 0
279        rec = self.cur.first()
280        while rec is not None:
281            if type(self.keytype) == type(''):
282                self.assertTrue(int(rec[0]))  # for primary db, key is a number
283            else:
284                self.assertTrue(rec[0] and type(rec[0]) == type(0))
285            count = count + 1
286            if verbose:
287                print(rec)
288            rec = getattr(self.cur, "next")()
289        self.assertEqual(count, len(musicdata))  # all items accounted for
290
291
292        if verbose:
293            print("Secondary key traversal:")
294        self.cur = secDB.cursor(txn)
295        count = 0
296
297        # test cursor pget
298        vals = self.cur.pget('Unknown', flags=db.DB_LAST)
299        self.assertTrue(vals[1] == 99 or vals[1] == '99', vals)
300        self.assertEqual(vals[0], 'Unknown')
301        vals[2].index('Unknown')
302        vals[2].index('Unnamed')
303        vals[2].index('unknown')
304
305        vals = self.cur.pget('Unknown', data='wrong value', flags=db.DB_GET_BOTH)
306        self.assertEqual(vals, None, vals)
307
308        rec = self.cur.first()
309        self.assertEqual(rec[0], "Jazz")
310        while rec is not None:
311            count = count + 1
312            if verbose:
313                print(rec)
314            rec = getattr(self.cur, "next")()
315        # all items accounted for EXCEPT for 1 with "Blues" genre
316        self.assertEqual(count, len(musicdata)-1)
317
318        self.cur = None
319
320    def getGenre(self, priKey, priData):
321        self.assertEqual(type(priData), type(""))
322        genre = priData.split('|')[2]
323
324        if verbose:
325            print('getGenre key: %r data: %r' % (priKey, priData))
326
327        if genre == 'Blues':
328            return db.DB_DONOTINDEX
329        else:
330            return genre
331
332    def getGenreList(self, priKey, PriData) :
333        v = self.getGenre(priKey, PriData)
334        if type(v) == type("") :
335            v = [v]
336        return v
337
338
339#----------------------------------------------------------------------
340
341
342class AssociateHashTestCase(AssociateTestCase):
343    dbtype = db.DB_HASH
344
345class AssociateBTreeTestCase(AssociateTestCase):
346    dbtype = db.DB_BTREE
347
348class AssociateRecnoTestCase(AssociateTestCase):
349    dbtype = db.DB_RECNO
350    keytype = 0
351
352#----------------------------------------------------------------------
353
354class AssociateBTreeTxnTestCase(AssociateBTreeTestCase):
355    envFlags = db.DB_INIT_TXN
356    dbFlags = 0
357
358    def txn_finish_test(self, sDB, txn):
359        try:
360            self.finish_test(sDB, txn=txn)
361        finally:
362            if self.cur:
363                self.cur.close()
364                self.cur = None
365            if txn:
366                txn.commit()
367
368    def test13_associate_in_transaction(self):
369        if verbose:
370            print('\n', '-=' * 30)
371            print("Running %s.test13_associateAutoCommit..." % \
372                  self.__class__.__name__)
373
374        txn = self.env.txn_begin()
375        try:
376            self.createDB(txn=txn)
377
378            self.secDB = db.DB(self.env)
379            self.secDB.set_flags(db.DB_DUP)
380            self.secDB.set_get_returns_none(2)
381            self.secDB.open(self.filename, "secondary", db.DB_BTREE,
382                       db.DB_CREATE | db.DB_THREAD, txn=txn)
383            self.getDB().associate(self.secDB, self.getGenre, txn=txn)
384
385            self.addDataToDB(self.getDB(), txn=txn)
386        except:
387            txn.abort()
388            raise
389
390        self.txn_finish_test(self.secDB, txn=txn)
391
392
393#----------------------------------------------------------------------
394
395class ShelveAssociateTestCase(AssociateTestCase):
396
397    def createDB(self):
398        self.primary = dbshelve.open(self.filename,
399                                     dbname="primary",
400                                     dbenv=self.env,
401                                     filetype=self.dbtype)
402
403    def addDataToDB(self, d):
404        for key, value in list(musicdata.items()):
405            if type(self.keytype) == type(''):
406                key = "%02d" % key
407            d.put(key, value)    # save the value as is this time
408
409
410    def getGenre(self, priKey, priData):
411        self.assertEqual(type(priData), type(()))
412        if verbose:
413            print('getGenre key: %r data: %r' % (priKey, priData))
414        genre = priData[2]
415        if genre == 'Blues':
416            return db.DB_DONOTINDEX
417        else:
418            return genre
419
420
421class ShelveAssociateHashTestCase(ShelveAssociateTestCase):
422    dbtype = db.DB_HASH
423
424class ShelveAssociateBTreeTestCase(ShelveAssociateTestCase):
425    dbtype = db.DB_BTREE
426
427class ShelveAssociateRecnoTestCase(ShelveAssociateTestCase):
428    dbtype = db.DB_RECNO
429    keytype = 0
430
431
432#----------------------------------------------------------------------
433
434class ThreadedAssociateTestCase(AssociateTestCase):
435
436    def addDataToDB(self, d):
437        t1 = Thread(target = self.writer1,
438                    args = (d, ))
439        t2 = Thread(target = self.writer2,
440                    args = (d, ))
441
442        t1.setDaemon(True)
443        t2.setDaemon(True)
444        t1.start()
445        t2.start()
446        t1.join()
447        t2.join()
448
449    def writer1(self, d):
450        for key, value in list(musicdata.items()):
451            if type(self.keytype) == type(''):
452                key = "%02d" % key
453            d.put(key, '|'.join(value))
454
455    def writer2(self, d):
456        for x in range(100, 600):
457            key = 'z%2d' % x
458            value = [key] * 4
459            d.put(key, '|'.join(value))
460
461
462class ThreadedAssociateHashTestCase(ShelveAssociateTestCase):
463    dbtype = db.DB_HASH
464
465class ThreadedAssociateBTreeTestCase(ShelveAssociateTestCase):
466    dbtype = db.DB_BTREE
467
468class ThreadedAssociateRecnoTestCase(ShelveAssociateTestCase):
469    dbtype = db.DB_RECNO
470    keytype = 0
471
472
473#----------------------------------------------------------------------
474
475def test_suite():
476    suite = unittest.TestSuite()
477
478    suite.addTest(unittest.makeSuite(AssociateErrorTestCase))
479
480    suite.addTest(unittest.makeSuite(AssociateHashTestCase))
481    suite.addTest(unittest.makeSuite(AssociateBTreeTestCase))
482    suite.addTest(unittest.makeSuite(AssociateRecnoTestCase))
483
484    suite.addTest(unittest.makeSuite(AssociateBTreeTxnTestCase))
485
486    suite.addTest(unittest.makeSuite(ShelveAssociateHashTestCase))
487    suite.addTest(unittest.makeSuite(ShelveAssociateBTreeTestCase))
488    suite.addTest(unittest.makeSuite(ShelveAssociateRecnoTestCase))
489
490    if have_threads:
491        suite.addTest(unittest.makeSuite(ThreadedAssociateHashTestCase))
492        suite.addTest(unittest.makeSuite(ThreadedAssociateBTreeTestCase))
493        suite.addTest(unittest.makeSuite(ThreadedAssociateRecnoTestCase))
494
495    return suite
496
497
498if __name__ == '__main__':
499    unittest.main(defaultTest='test_suite')
500