%PDF- %PDF-
Direktori : /usr/lib/calibre/calibre/db/tests/ |
Current File : //usr/lib/calibre/calibre/db/tests/reading.py |
#!/usr/bin/env python3 __license__ = 'GPL v3' __copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>' __docformat__ = 'restructuredtext en' import datetime from io import BytesIO from time import time from calibre.utils.date import utc_tz from calibre.utils.localization import calibre_langcode_to_name from calibre.db.tests.base import BaseTest from polyglot.builtins import iteritems, itervalues def p(x): return datetime.datetime.strptime(x, '%Y-%m-%d').replace(tzinfo=utc_tz) class ReadingTest(BaseTest): def test_read(self): # {{{ 'Test the reading of data from the database' cache = self.init_cache(self.library_path) tests = { 3 : { 'title': 'Unknown', 'sort': 'Unknown', 'authors': ('Unknown',), 'author_sort': 'Unknown', 'series' : None, 'series_index': 1.0, 'rating': None, 'tags': (), 'formats':(), 'identifiers': {}, 'timestamp': datetime.datetime(2011, 9, 7, 19, 54, 41, tzinfo=utc_tz), 'pubdate': datetime.datetime(2011, 9, 7, 19, 54, 41, tzinfo=utc_tz), 'last_modified': datetime.datetime(2011, 9, 7, 19, 54, 41, tzinfo=utc_tz), 'publisher': None, 'languages': (), 'comments': None, '#enum': None, '#authors':(), '#date':None, '#rating':None, '#series':None, '#series_index': None, '#tags':(), '#yesno':None, '#comments': None, 'size':None, }, 2 : { 'title': 'Title One', 'sort': 'One', 'authors': ('Author One',), 'author_sort': 'One, Author', 'series' : 'A Series One', 'series_index': 1.0, 'tags':('Tag One', 'Tag Two'), 'formats': ('FMT1',), 'rating': 4.0, 'identifiers': {'test':'one'}, 'timestamp': datetime.datetime(2011, 9, 5, 21, 6, tzinfo=utc_tz), 'pubdate': datetime.datetime(2011, 9, 5, 21, 6, tzinfo=utc_tz), 'publisher': 'Publisher One', 'languages': ('eng',), 'comments': '<p>Comments One</p>', '#enum':'One', '#authors':('Custom One', 'Custom Two'), '#date':datetime.datetime(2011, 9, 5, 6, 0, tzinfo=utc_tz), '#rating':2.0, '#series':'My Series One', '#series_index': 1.0, '#tags':('My Tag One', 'My Tag Two'), '#yesno':True, '#comments': '<div>My Comments One<p></p></div>', 'size':9, }, 1 : { 'title': 'Title Two', 'sort': 'Title Two', 'authors': ('Author Two', 'Author One'), 'author_sort': 'Two, Author & One, Author', 'series' : 'A Series One', 'series_index': 2.0, 'rating': 6.0, 'tags': ('Tag One', 'News'), 'formats':('FMT1', 'FMT2'), 'identifiers': {'test':'two'}, 'timestamp': datetime.datetime(2011, 9, 6, 6, 0, tzinfo=utc_tz), 'pubdate': datetime.datetime(2011, 8, 5, 6, 0, tzinfo=utc_tz), 'publisher': 'Publisher Two', 'languages': ('deu',), 'comments': '<p>Comments Two</p>', '#enum':'Two', '#authors':('My Author Two',), '#date':datetime.datetime(2011, 9, 1, 6, 0, tzinfo=utc_tz), '#rating':4.0, '#series':'My Series Two', '#series_index': 3.0, '#tags':('My Tag Two',), '#yesno':False, '#comments': '<div>My Comments Two<p></p></div>', 'size':9, }, } for book_id, test in iteritems(tests): for field, expected_val in iteritems(test): val = cache.field_for(field, book_id) if isinstance(val, tuple) and 'authors' not in field and 'languages' not in field: val, expected_val = set(val), set(expected_val) self.assertEqual(expected_val, val, 'Book id: %d Field: %s failed: %r != %r'%( book_id, field, expected_val, val)) # }}} def test_sorting(self): # {{{ 'Test sorting' cache = self.init_cache() ae = self.assertEqual lmap = {x:cache.field_for('languages', x) for x in (1, 2, 3)} lq = sorted(lmap, key=lambda x: calibre_langcode_to_name((lmap[x] or ('',))[0])) for field, order in iteritems({ 'title' : [2, 1, 3], 'authors': [2, 1, 3], 'series' : [3, 1, 2], 'tags' : [3, 1, 2], 'rating' : [3, 2, 1], # 'identifiers': [3, 2, 1], There is no stable sort since 1 and # 2 have the same identifier keys # 'last_modified': [3, 2, 1], There is no stable sort as two # records have the exact same value 'timestamp': [2, 1, 3], 'pubdate' : [1, 2, 3], 'publisher': [3, 2, 1], 'languages': lq, 'comments': [3, 2, 1], '#enum' : [3, 2, 1], '#authors' : [3, 2, 1], '#date': [3, 1, 2], '#rating':[3, 2, 1], '#series':[3, 2, 1], '#tags':[3, 2, 1], '#yesno':[2, 1, 3], '#comments':[3, 2, 1], 'id': [1, 2, 3], }): x = list(reversed(order)) ae(order, cache.multisort([(field, True)], ids_to_sort=x), 'Ascending sort of %s failed'%field) ae(x, cache.multisort([(field, False)], ids_to_sort=order), 'Descending sort of %s failed'%field) # Test sorting of is_multiple fields. # Author like fields should be sorted by generating sort names from the # actual values in entry order for field in ('authors', '#authors'): ae( cache.set_field(field, {1:('aa bb', 'bb cc', 'cc dd'), 2:('bb aa', 'xx yy'), 3: ('aa bb', 'bb aa')}), {1, 2, 3}) ae([2, 3, 1], cache.multisort([(field, True)], ids_to_sort=(1, 2, 3))) ae([1, 3, 2], cache.multisort([(field, False)], ids_to_sort=(1, 2, 3))) # All other is_multiple fields should be sorted by sorting the values # for each book and using that as the sort key for field in ('tags', '#tags'): ae( cache.set_field(field, {1:('b', 'a'), 2:('c', 'y'), 3: ('b', 'z')}), {1, 2, 3}) ae([1, 3, 2], cache.multisort([(field, True)], ids_to_sort=(1, 2, 3))) ae([2, 3, 1], cache.multisort([(field, False)], ids_to_sort=(1, 2, 3))) # Test tweak to sort dates by visible format from calibre.utils.config_base import Tweak ae(cache.set_field('pubdate', {1:p('2001-3-3'), 2:p('2002-2-3'), 3:p('2003-1-3')}), {1, 2, 3}) ae([1, 2, 3], cache.multisort([('pubdate', True)])) with Tweak('gui_pubdate_display_format', 'MMM'), Tweak('sort_dates_using_visible_fields', True): c2 = self.init_cache() ae([3, 2, 1], c2.multisort([('pubdate', True)])) # Test bool sorting when not tristate cache.set_pref('bools_are_tristate', False) c2 = self.init_cache() ae([2, 3, 1], c2.multisort([('#yesno', True), ('id', False)])) # Test subsorting ae([3, 2, 1], cache.multisort([('identifiers', True), ('title', True)]), 'Subsort failed') from calibre.ebooks.metadata.book.base import Metadata for i in range(7): cache.create_book_entry(Metadata('title%d' % i), apply_import_tags=False) cache.create_custom_column('one', 'CC1', 'int', False) cache.create_custom_column('two', 'CC2', 'int', False) cache.create_custom_column('three', 'CC3', 'int', False) cache.close() cache = self.init_cache() cache.set_field('#one', {(i+(5*m)):m for m in (0, 1) for i in range(1, 6)}) cache.set_field('#two', {i+(m*3):m for m in (0, 1, 2) for i in (1, 2, 3)}) cache.set_field('#two', {10:2}) cache.set_field('#three', {i:i for i in range(1, 11)}) ae(list(range(1, 11)), cache.multisort([('#one', True), ('#two', True)], ids_to_sort=sorted(cache.all_book_ids()))) ae([4, 5, 1, 2, 3, 7,8, 9, 10, 6], cache.multisort([('#one', True), ('#two', False)], ids_to_sort=sorted(cache.all_book_ids()))) ae([5, 4, 3, 2, 1, 10, 9, 8, 7, 6], cache.multisort([('#one', True), ('#two', False), ('#three', False)], ids_to_sort=sorted(cache.all_book_ids()))) # }}} def test_get_metadata(self): # {{{ 'Test get_metadata() returns the same data for both backends' from calibre.library.database2 import LibraryDatabase2 old = LibraryDatabase2(self.library_path) old_metadata = {i:old.get_metadata( i, index_is_id=True, get_cover=True, cover_as_data=True) for i in range(1, 4)} for mi in itervalues(old_metadata): mi.format_metadata = dict(mi.format_metadata) if mi.formats: mi.formats = tuple(mi.formats) old.conn.close() old = None cache = self.init_cache(self.library_path) new_metadata = {i:cache.get_metadata( i, get_cover=True, cover_as_data=True) for i in range(1, 4)} cache = None for mi2, mi1 in zip(list(new_metadata.values()), list(old_metadata.values())): self.compare_metadata(mi1, mi2) # }}} def test_serialize_metadata(self): # {{{ from calibre.utils.serialize import json_dumps, json_loads, msgpack_dumps, msgpack_loads from calibre.library.field_metadata import fm_as_dict cache = self.init_cache(self.library_path) fm = cache.field_metadata for d, l in ((json_dumps, json_loads), (msgpack_dumps, msgpack_loads)): fm2 = l(d(fm)) self.assertEqual(fm_as_dict(fm), fm_as_dict(fm2)) for i in range(1, 4): mi = cache.get_metadata(i, get_cover=True, cover_as_data=True) rmi = msgpack_loads(msgpack_dumps(mi)) self.compare_metadata(mi, rmi, exclude='format_metadata has_cover formats id'.split()) rmi = json_loads(json_dumps(mi)) self.compare_metadata(mi, rmi, exclude='format_metadata has_cover formats id'.split()) # }}} def test_get_cover(self): # {{{ 'Test cover() returns the same data for both backends' from calibre.library.database2 import LibraryDatabase2 old = LibraryDatabase2(self.library_path) covers = {i: old.cover(i, index_is_id=True) for i in old.all_ids()} old.conn.close() old = None cache = self.init_cache(self.library_path) for book_id, cdata in iteritems(covers): self.assertEqual(cdata, cache.cover(book_id), 'Reading of cover failed') f = cache.cover(book_id, as_file=True) self.assertEqual(cdata, f.read() if f else f, 'Reading of cover as file failed') if cdata: with open(cache.cover(book_id, as_path=True), 'rb') as f: self.assertEqual(cdata, f.read(), 'Reading of cover as path failed') else: self.assertEqual(cdata, cache.cover(book_id, as_path=True), 'Reading of null cover as path failed') buf = BytesIO() self.assertFalse(cache.copy_cover_to(99999, buf), 'copy_cover_to() did not return False for non-existent book_id') self.assertFalse(cache.copy_cover_to(3, buf), 'copy_cover_to() did not return False for non-existent cover') # }}} def test_searching(self): # {{{ 'Test searching returns the same data for both backends' from calibre.library.database2 import LibraryDatabase2 old = LibraryDatabase2(self.library_path) oldvals = {query:set(old.search_getting_ids(query, '')) for query in ( # Date tests 'date:9/6/2011', 'date:true', 'date:false', 'pubdate:1/9/2011', '#date:true', 'date:<100_daysago', 'date:>9/6/2011', '#date:>9/1/2011', '#date:=2011', # Number tests 'rating:3', 'rating:>2', 'rating:=2', 'rating:true', 'rating:false', 'rating:>4', 'tags:#<2', 'tags:#>7', 'cover:false', 'cover:true', '#float:>11', '#float:<1k', '#float:10.01', '#float:false', 'series_index:1', 'series_index:<3', # Bool tests '#yesno:true', '#yesno:false', '#yesno:_yes', '#yesno:_no', '#yesno:_empty', # Keypair tests 'identifiers:true', 'identifiers:false', 'identifiers:test', 'identifiers:test:false', 'identifiers:test:one', 'identifiers:t:n', 'identifiers:=test:=two', 'identifiers:x:y', 'identifiers:z', # Text tests 'title:="Title One"', 'title:~title', '#enum:=one', '#enum:tw', '#enum:false', '#enum:true', 'series:one', 'tags:one', 'tags:true', 'tags:false', 'uuid:2', 'one', '20.02', '"publisher one"', '"my comments one"', 'series_sort:one', # User categories '@Good Authors:One', '@Good Series.good tags:two', # Cover/Formats 'cover:true', 'cover:false', 'formats:true', 'formats:false', 'formats:#>1', 'formats:#=1', 'formats:=fmt1', 'formats:=fmt2', 'formats:=fmt1 or formats:fmt2', '#formats:true', '#formats:false', '#formats:fmt1', '#formats:fmt2', '#formats:fmt1 and #formats:fmt2', )} old.conn.close() old = None cache = self.init_cache(self.cloned_library) for query, ans in iteritems(oldvals): nr = cache.search(query, '') self.assertEqual(ans, nr, 'Old result: %r != New result: %r for search: %s'%( ans, nr, query)) # Test searching by id, which was introduced in the new backend self.assertEqual(cache.search('id:1', ''), {1}) self.assertEqual(cache.search('id:>1', ''), {2, 3}) # Numeric/rating searches with relops in the old db were incorrect, so # test them specifically here cache.set_field('rating', {1:4, 2:2, 3:5}) self.assertEqual(cache.search('rating:>2'), set()) self.assertEqual(cache.search('rating:>=2'), {1, 3}) self.assertEqual(cache.search('rating:<2'), {2}) self.assertEqual(cache.search('rating:<=2'), {1, 2, 3}) self.assertEqual(cache.search('rating:<=2'), {1, 2, 3}) self.assertEqual(cache.search('rating:=2'), {1, 3}) self.assertEqual(cache.search('rating:2'), {1, 3}) self.assertEqual(cache.search('rating:!=2'), {2}) cache.field_metadata.all_metadata()['#rating']['display']['allow_half_stars'] = True cache.set_field('#rating', {1:3, 2:4, 3:9}) self.assertEqual(cache.search('#rating:1'), set()) self.assertEqual(cache.search('#rating:1.5'), {1}) self.assertEqual(cache.search('#rating:>4'), {3}) self.assertEqual(cache.search('#rating:2'), {2}) # template searches # Test text search self.assertEqual(cache.search('template:"{#formats}#@#:t:fmt1"'), {1,2}) self.assertEqual(cache.search('template:"{authors}#@#:t:=Author One"'), {2}) cache.set_field('pubdate', {1:p('2001-02-06'), 2:p('2001-10-06'), 3:p('2001-06-06')}) cache.set_field('timestamp', {1:p('2002-02-06'), 2:p('2000-10-06'), 3:p('2001-06-06')}) # Test numeric compare search self.assertEqual(cache.search("template:\"program: " "floor(days_between(field(\'pubdate\'), " "field(\'timestamp\')))#@#:n:>0\""), {2,3}) # Test date search self.assertEqual(cache.search('template:{pubdate}#@#:d:<2001-09-01"'), {1,3}) # Test boolean search self.assertEqual(cache.search('template:{series}#@#:b:true'), {1,2}) self.assertEqual(cache.search('template:{series}#@#:b:false'), {3}) # Note that the old db searched uuid for un-prefixed searches, the new # db does not, for performance # }}} def test_get_categories(self): # {{{ 'Check that get_categories() returns the same data for both backends' from calibre.library.database2 import LibraryDatabase2 old = LibraryDatabase2(self.library_path) old_categories = old.get_categories() old.conn.close() cache = self.init_cache(self.library_path) new_categories = cache.get_categories() self.assertEqual(set(old_categories), set(new_categories), 'The set of old categories is not the same as the set of new categories') def compare_category(category, old, new): for attr in ('name', 'original_name', 'id', 'count', 'is_hierarchical', 'is_editable', 'is_searchable', 'id_set', 'avg_rating', 'sort', 'use_sort_as_name', 'category'): oval, nval = getattr(old, attr), getattr(new, attr) if ( (category in {'rating', '#rating'} and attr in {'id_set', 'sort'}) or (category == 'series' and attr == 'sort') or # Sorting is wrong in old (category == 'identifiers' and attr == 'id_set') or (category == '@Good Series') or # Sorting is wrong in old (category == 'news' and attr in {'count', 'id_set'}) or (category == 'formats' and attr == 'id_set') ): continue self.assertEqual(oval, nval, 'The attribute %s for %s in category %s does not match. Old is %r, New is %r' %(attr, old.name, category, oval, nval)) for category in old_categories: old, new = old_categories[category], new_categories[category] self.assertEqual(len(old), len(new), 'The number of items in the category %s is not the same'%category) for o, n in zip(old, new): compare_category(category, o, n) # }}} def test_get_formats(self): # {{{ 'Test reading ebook formats using the format() method' from calibre.library.database2 import LibraryDatabase2 from calibre.db.cache import NoSuchFormat old = LibraryDatabase2(self.library_path) ids = old.all_ids() lf = {i:set(old.formats(i, index_is_id=True).split(',')) if old.formats( i, index_is_id=True) else set() for i in ids} formats = {i:{f:old.format(i, f, index_is_id=True) for f in fmts} for i, fmts in iteritems(lf)} old.conn.close() old = None cache = self.init_cache(self.library_path) for book_id, fmts in iteritems(lf): self.assertEqual(fmts, set(cache.formats(book_id)), 'Set of formats is not the same') for fmt in fmts: old = formats[book_id][fmt] self.assertEqual(old, cache.format(book_id, fmt), 'Old and new format disagree') f = cache.format(book_id, fmt, as_file=True) self.assertEqual(old, f.read(), 'Failed to read format as file') with open(cache.format(book_id, fmt, as_path=True, preserve_filename=True), 'rb') as f: self.assertEqual(old, f.read(), 'Failed to read format as path') with open(cache.format(book_id, fmt, as_path=True), 'rb') as f: self.assertEqual(old, f.read(), 'Failed to read format as path') buf = BytesIO() self.assertRaises(NoSuchFormat, cache.copy_format_to, 99999, 'X', buf, 'copy_format_to() failed to raise an exception for non-existent book') self.assertRaises(NoSuchFormat, cache.copy_format_to, 1, 'X', buf, 'copy_format_to() failed to raise an exception for non-existent format') # }}} def test_author_sort_for_authors(self): # {{{ 'Test getting the author sort for authors from the db' cache = self.init_cache() table = cache.fields['authors'].table table.set_sort_names({next(iter(table.id_map)): 'Fake Sort'}, cache.backend) authors = tuple(itervalues(table.id_map)) nval = cache.author_sort_from_authors(authors) self.assertIn('Fake Sort', nval) db = self.init_old() self.assertEqual(db.author_sort_from_authors(authors), nval) db.close() del db # }}} def test_get_next_series_num(self): # {{{ 'Test getting the next series number for a series' cache = self.init_cache() cache.set_field('series', {3:'test series'}) cache.set_field('series_index', {3:13}) table = cache.fields['series'].table series = tuple(itervalues(table.id_map)) nvals = {s:cache.get_next_series_num_for(s) for s in series} db = self.init_old() self.assertEqual({s:db.get_next_series_num_for(s) for s in series}, nvals) db.close() # }}} def test_has_book(self): # {{{ 'Test detecting duplicates' from calibre.ebooks.metadata.book.base import Metadata cache = self.init_cache() db = self.init_old() for title in itervalues(cache.fields['title'].table.book_col_map): for x in (db, cache): self.assertTrue(x.has_book(Metadata(title))) self.assertTrue(x.has_book(Metadata(title.upper()))) self.assertFalse(x.has_book(Metadata(title + 'XXX'))) self.assertFalse(x.has_book(Metadata(title[:1]))) db.close() # }}} def test_datetime(self): # {{{ ' Test the reading of datetimes stored in the db ' from calibre.utils.date import parse_date from calibre.db.tables import c_parse, UNDEFINED_DATE, _c_speedup # First test parsing of string to UTC time for raw in ('2013-07-22 15:18:29+05:30', ' 2013-07-22 15:18:29+00:00', '2013-07-22 15:18:29', '2003-09-21 23:30:00-06:00'): self.assertTrue(_c_speedup(raw)) ctime = c_parse(raw) pytime = parse_date(raw, assume_utc=True) self.assertEqual(ctime, pytime) self.assertEqual(c_parse(2003).year, 2003) for x in (None, '', 'abc'): self.assertEqual(UNDEFINED_DATE, c_parse(x)) # }}} def test_restrictions(self): # {{{ ' Test searching with and without restrictions ' cache = self.init_cache() se = self.assertSetEqual se(cache.all_book_ids(), cache.search('')) se({1, 2}, cache.search('', 'not authors:=Unknown')) se(set(), cache.search('authors:=Unknown', 'not authors:=Unknown')) se({2}, cache.search('not authors:"=Author Two"', 'not authors:=Unknown')) se({2}, cache.search('not authors:"=Author Two"', book_ids={1, 2})) se({2}, cache.search('not authors:"=Author Two"', 'not authors:=Unknown', book_ids={1,2,3})) se(set(), cache.search('authors:=Unknown', 'not authors:=Unknown', book_ids={1,2,3})) se(cache.all_book_ids(), cache.books_in_virtual_library('')) se(cache.all_book_ids(), cache.books_in_virtual_library('does not exist')) cache.set_pref('virtual_libraries', {'1':'title:"=Title One"', '12':'id:1 or id:2'}) se({2}, cache.books_in_virtual_library('1')) se({1,2}, cache.books_in_virtual_library('12')) se({1}, cache.books_in_virtual_library('12', 'id:1')) se({2}, cache.books_in_virtual_library('1', 'id:1 or id:2')) # }}} def test_search_caching(self): # {{{ ' Test caching of searches ' from calibre.db.search import LRUCache class TestCache(LRUCache): hit_counter = 0 miss_counter = 0 def get(self, key, default=None): ans = LRUCache.get(self, key, default=default) if ans is not None: self.hit_counter += 1 else: self.miss_counter += 1 @property def cc(self): self.hit_counter = self.miss_counter = 0 @property def counts(self): return self.hit_counter, self.miss_counter cache = self.init_cache() cache._search_api.cache = c = TestCache() ae = self.assertEqual def test(hit, result, *args, **kw): c.cc num = kw.get('num', 2) ae(cache.search(*args), result) ae(c.counts, (num, 0) if hit else (0, num)) c.cc test(False, {3}, 'Unknown') test(True, {3}, 'Unknown') test(True, {3}, 'Unknown') cache._search_api.MAX_CACHE_UPDATE = 0 cache.set_field('title', {3:'xxx'}) test(False, {3}, 'Unknown') # cache cleared test(True, {3}, 'Unknown') c.limit = 5 for i in range(6): test(False, set(), 'nomatch_%s' % i) test(False, {3}, 'Unknown') # cached search expired test(False, {3}, '', 'unknown', num=1) test(True, {3}, '', 'unknown', num=1) test(True, {3}, 'Unknown', 'unknown', num=1) cache._search_api.MAX_CACHE_UPDATE = 100 test(False, {2, 3}, 'title:=xxx or title:"=Title One"') cache.set_field('publisher', {3:'ppppp', 2:'other'}) # Test cache update worked test(True, {2, 3}, 'title:=xxx or title:"=Title One"') # }}} def test_proxy_metadata(self): # {{{ ' Test the ProxyMetadata object used for composite columns ' from calibre.ebooks.metadata.book.base import STANDARD_METADATA_FIELDS cache = self.init_cache() for book_id in cache.all_book_ids(): mi = cache.get_metadata(book_id, get_user_categories=True) pmi = cache.get_proxy_metadata(book_id) self.assertSetEqual(set(mi.custom_field_keys()), set(pmi.custom_field_keys())) for field in STANDARD_METADATA_FIELDS | {'#series_index'}: f = lambda x: x if field == 'formats': f = lambda x: x if x is None else tuple(x) self.assertEqual(f(getattr(mi, field)), f(getattr(pmi, field)), f'Standard field: {field} not the same for book {book_id}') self.assertEqual(mi.format_field(field), pmi.format_field(field), f'Standard field format: {field} not the same for book {book_id}') def f(x): try: x.pop('rec_index', None) except: pass return x if field not in {'#series_index'}: v = pmi.get_standard_metadata(field) self.assertTrue(v is None or isinstance(v, dict)) self.assertEqual(f(mi.get_standard_metadata(field, False)), f(v), 'get_standard_metadata() failed for field %s' % field) for field, meta in cache.field_metadata.custom_iteritems(): if meta['datatype'] != 'composite': self.assertEqual(f(getattr(mi, field)), f(getattr(pmi, field)), f'Custom field: {field} not the same for book {book_id}') self.assertEqual(mi.format_field(field), pmi.format_field(field), f'Custom field format: {field} not the same for book {book_id}') # Test handling of recursive templates cache.create_custom_column('comp2', 'comp2', 'composite', False, display={'composite_template':'{title}'}) cache.create_custom_column('comp1', 'comp1', 'composite', False, display={'composite_template':'foo{#comp2}'}) cache.close() cache = self.init_cache() mi, pmi = cache.get_metadata(1), cache.get_proxy_metadata(1) self.assertEqual(mi.get('#comp1'), pmi.get('#comp1')) # }}} def test_marked_field(self): # {{{ ' Test the marked field ' db = self.init_legacy() db.set_marked_ids({3:1, 2:3}) ids = [1,2,3] db.multisort([('marked', True)], only_ids=ids) self.assertListEqual([1, 3, 2], ids) db.multisort([('marked', False)], only_ids=ids) self.assertListEqual([2, 3, 1], ids) # }}} def test_composites(self): # {{{ ' Test sorting and searching in composite columns ' cache = self.init_cache() cache.create_custom_column('mult', 'CC1', 'composite', True, display={'composite_template': 'b,a,c'}) cache.create_custom_column('single', 'CC2', 'composite', False, display={'composite_template': 'b,a,c'}) cache.create_custom_column('number', 'CC3', 'composite', False, display={'composite_template': '{#float}', 'composite_sort':'number'}) cache.create_custom_column('size', 'CC4', 'composite', False, display={'composite_template': '{#float:human_readable()}', 'composite_sort':'number'}) cache.create_custom_column('ccdate', 'CC5', 'composite', False, display={'composite_template': "{:'format_date(raw_field('pubdate'), 'dd-MM-yy')'}", 'composite_sort':'date'}) cache.create_custom_column('bool', 'CC6', 'composite', False, display={'composite_template': '{#yesno}', 'composite_sort':'bool'}) cache.create_custom_column('ccm', 'CC7', 'composite', True, display={'composite_template': '{#tags}'}) cache.create_custom_column('ccp', 'CC8', 'composite', True, display={'composite_template': '{publisher}'}) cache.create_custom_column('ccf', 'CC9', 'composite', True, display={'composite_template': "{:'approximate_formats()'}"}) cache = self.init_cache() # Test searching self.assertEqual({1,2,3}, cache.search('#mult:=a')) self.assertEqual(set(), cache.search('#mult:=b,a,c')) self.assertEqual({1,2,3}, cache.search('#single:=b,a,c')) self.assertEqual(set(), cache.search('#single:=b')) # Test numeric sorting cache.set_field('#float', {1:2, 2:10, 3:0.0001}) self.assertEqual([3, 1, 2], cache.multisort([('#number', True)])) cache.set_field('#float', {1:3, 2:2*1024, 3:1*1024*1024}) self.assertEqual([1, 2, 3], cache.multisort([('#size', True)])) # Test date sorting cache.set_field('pubdate', {1:p('2001-02-06'), 2:p('2001-10-06'), 3:p('2001-06-06')}) self.assertEqual([1, 3, 2], cache.multisort([('#ccdate', True)])) # Test bool sorting self.assertEqual([2, 1, 3], cache.multisort([('#bool', True)])) # Test is_multiple sorting cache.set_field('#tags', {1:'b, a, c', 2:'a, b, c', 3:'a, c, b'}) self.assertEqual([1, 2, 3], cache.multisort([('#ccm', True)])) # Test that lock downgrading during update of search cache works self.assertEqual(cache.search('#ccp:One'), {2}) cache.set_field('publisher', {2:'One', 1:'One'}) self.assertEqual(cache.search('#ccp:One'), {1, 2}) self.assertEqual(cache.search('#ccf:FMT1'), {1, 2}) cache.remove_formats({1:('FMT1',)}) self.assertEqual('FMT2', cache.field_for('#ccf', 1)) # }}} def test_find_identical_books(self): # {{{ ' Test find_identical_books ' from calibre.ebooks.metadata.book.base import Metadata from calibre.db.utils import find_identical_books # 'find_identical_books': [(,), (Metadata('unknown'),), (Metadata('xxxx'),)], cache = self.init_cache(self.library_path) cache.set_field('languages', {1: ('fra', 'deu')}) data = cache.data_for_find_identical_books() lm = cache.get_metadata(1) lm2 = cache.get_metadata(1) lm2.languages = ['eng'] for mi, books in ( (Metadata('title one', ['author one']), {2}), (Metadata('Unknown', ['Unknown']), {3}), (Metadata('title two', ['author one']), {1}), (lm, {1}), (lm2, set()), ): self.assertEqual(books, cache.find_identical_books(mi)) self.assertEqual(books, find_identical_books(mi, data)) # }}} def test_last_read_positions(self): # {{{ cache = self.init_cache(self.library_path) self.assertFalse(cache.get_last_read_positions(1, 'x', 'u')) self.assertRaises(Exception, cache.set_last_read_position, 12, 'x', cfi='c') epoch = time() cache.set_last_read_position(1, 'EPUB', 'user', 'device', 'cFi', epoch, 0.3) self.assertFalse(cache.get_last_read_positions(1, 'x', 'u')) self.assertEqual(cache.get_last_read_positions(1, 'ePuB', 'user'), [{'epoch':epoch, 'device':'device', 'cfi':'cFi', 'pos_frac':0.3}]) cache.set_last_read_position(1, 'EPUB', 'user', 'device') self.assertFalse(cache.get_last_read_positions(1, 'ePuB', 'user')) # }}} def test_storing_conversion_options(self): # {{{ cache = self.init_cache(self.library_path) opts = {1: b'binary', 2: 'unicode'} cache.set_conversion_options(opts, 'PIPE') for book_id, val in iteritems(opts): got = cache.conversion_options(book_id, 'PIPE') if not isinstance(val, bytes): val = val.encode('utf-8') self.assertEqual(got, val) # }}}