%PDF- %PDF-
Direktori : /usr/lib/calibre/calibre/db/tests/ |
Current File : //usr/lib/calibre/calibre/db/tests/writing.py |
#!/usr/bin/env python3 __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' __docformat__ = 'restructuredtext en' from collections import namedtuple from functools import partial from io import BytesIO from calibre.ebooks.metadata import author_to_author_sort, title_sort from calibre.ebooks.metadata.book.base import Metadata from calibre.utils.date import UNDEFINED_DATE from calibre.db.tests.base import BaseTest, IMG from calibre.db.backend import FTSQueryError from polyglot.builtins import iteritems, itervalues class WritingTest(BaseTest): # Utils {{{ def create_getter(self, name, getter=None): if getter is None: if name.endswith('_index'): ans = lambda db:partial(db.get_custom_extra, index_is_id=True, label=name[1:].replace('_index', '')) else: ans = lambda db:partial(db.get_custom, label=name[1:], index_is_id=True) else: ans = lambda db:partial(getattr(db, getter), index_is_id=True) return ans def create_setter(self, name, setter=None): if setter is None: ans = lambda db:partial(db.set_custom, label=name[1:], commit=True) else: ans = lambda db:partial(getattr(db, setter), commit=True) return ans def create_test(self, name, vals, getter=None, setter=None): T = namedtuple('Test', 'name vals getter setter') return T(name, vals, self.create_getter(name, getter), self.create_setter(name, setter)) def run_tests(self, tests): results = {} for test in tests: results[test] = [] for val in test.vals: cl = self.cloned_library cache = self.init_cache(cl) cache.set_field(test.name, {1: val}) cached_res = cache.field_for(test.name, 1) del cache db = self.init_old(cl) getter = test.getter(db) sqlite_res = getter(1) if test.name.endswith('_index'): val = float(val) if val is not None else 1.0 self.assertEqual(sqlite_res, val, 'Failed setting for %s with value %r, sqlite value not the same. val: %r != sqlite_val: %r'%( test.name, val, val, sqlite_res)) else: test.setter(db)(1, val) old_cached_res = getter(1) self.assertEqual(old_cached_res, cached_res, 'Failed setting for %s with value %r, cached value not the same. Old: %r != New: %r'%( test.name, val, old_cached_res, cached_res)) db.refresh() old_sqlite_res = getter(1) self.assertEqual(old_sqlite_res, sqlite_res, 'Failed setting for %s, sqlite value not the same: %r != %r'%( test.name, old_sqlite_res, sqlite_res)) del db # }}} def test_one_one(self): # {{{ 'Test setting of values in one-one fields' tests = [self.create_test('#yesno', (True, False, 'true', 'false', None))] for name, getter, setter in ( ('#series_index', None, None), ('series_index', 'series_index', 'set_series_index'), ('#float', None, None), ): vals = ['1.5', None, 0, 1.0] tests.append(self.create_test(name, tuple(vals), getter, setter)) for name, getter, setter in ( ('pubdate', 'pubdate', 'set_pubdate'), ('timestamp', 'timestamp', 'set_timestamp'), ('#date', None, None), ): tests.append(self.create_test( name, ('2011-1-12', UNDEFINED_DATE, None), getter, setter)) for name, getter, setter in ( ('title', 'title', 'set_title'), ('uuid', 'uuid', 'set_uuid'), ('author_sort', 'author_sort', 'set_author_sort'), ('sort', 'title_sort', 'set_title_sort'), ('#comments', None, None), ('comments', 'comments', 'set_comment'), ): vals = ['something', None] if name not in {'comments', '#comments'}: # Setting text column to '' returns None in the new backend # and '' in the old. I think None is more correct. vals.append('') if name == 'comments': # Again new behavior of deleting comment rather than setting # empty string is more correct. vals.remove(None) tests.append(self.create_test(name, tuple(vals), getter, setter)) self.run_tests(tests) # }}} def test_many_one_basic(self): # {{{ 'Test the different code paths for writing to a many-one field' cl = self.cloned_library cache = self.init_cache(cl) f = cache.fields['publisher'] item_ids = {f.ids_for_book(1)[0], f.ids_for_book(2)[0]} val = 'Changed' self.assertEqual(cache.set_field('publisher', {1:val, 2:val}), {1, 2}) cache2 = self.init_cache(cl) for book_id in (1, 2): for c in (cache, cache2): self.assertEqual(c.field_for('publisher', book_id), val) self.assertFalse(item_ids.intersection(set(c.fields['publisher'].table.id_map))) del cache2 self.assertFalse(cache.set_field('publisher', {1:val, 2:val})) val = val.lower() self.assertFalse(cache.set_field('publisher', {1:val, 2:val}, allow_case_change=False)) self.assertEqual(cache.set_field('publisher', {1:val, 2:val}), {1, 2}) cache2 = self.init_cache(cl) for book_id in (1, 2): for c in (cache, cache2): self.assertEqual(c.field_for('publisher', book_id), val) del cache2 self.assertEqual(cache.set_field('publisher', {1:'new', 2:'New'}), {1, 2}) self.assertEqual(cache.field_for('publisher', 1).lower(), 'new') self.assertEqual(cache.field_for('publisher', 2).lower(), 'new') self.assertEqual(cache.set_field('publisher', {1:None, 2:'NEW'}), {1, 2}) self.assertEqual(len(f.table.id_map), 1) self.assertEqual(cache.set_field('publisher', {2:None}), {2}) self.assertEqual(len(f.table.id_map), 0) cache2 = self.init_cache(cl) self.assertEqual(len(cache2.fields['publisher'].table.id_map), 0) del cache2 self.assertEqual(cache.set_field('publisher', {1:'one', 2:'two', 3:'three'}), {1, 2, 3}) self.assertEqual(cache.set_field('publisher', {1:''}), {1}) self.assertEqual(cache.set_field('publisher', {1:'two'}), {1}) self.assertEqual(tuple(map(f.for_book, (1,2,3))), ('two', 'two', 'three')) self.assertEqual(cache.set_field('publisher', {1:'Two'}), {1, 2}) cache2 = self.init_cache(cl) self.assertEqual(tuple(map(f.for_book, (1,2,3))), ('Two', 'Two', 'three')) del cache2 # Enum self.assertFalse(cache.set_field('#enum', {1:'Not allowed'})) self.assertEqual(cache.set_field('#enum', {1:'One', 2:'One', 3:'Three'}), {1, 3}) self.assertEqual(cache.set_field('#enum', {1:None}), {1}) cache2 = self.init_cache(cl) for c in (cache, cache2): for i, val in iteritems({1:None, 2:'One', 3:'Three'}): self.assertEqual(c.field_for('#enum', i), val) del cache2 # Rating self.assertFalse(cache.set_field('rating', {1:6, 2:4})) self.assertEqual(cache.set_field('rating', {1:0, 3:2}), {1, 3}) self.assertEqual(cache.set_field('#rating', {1:None, 2:4, 3:8}), {1, 2, 3}) cache2 = self.init_cache(cl) for c in (cache, cache2): for i, val in iteritems({1:None, 2:4, 3:2}): self.assertEqual(c.field_for('rating', i), val) for i, val in iteritems({1:None, 2:4, 3:8}): self.assertEqual(c.field_for('#rating', i), val) del cache2 # Series self.assertFalse(cache.set_field('series', {1:'a series one', 2:'a series one'}, allow_case_change=False)) self.assertEqual(cache.set_field('series', {3:'Series [3]'}), {3}) self.assertEqual(cache.set_field('#series', {1:'Series', 3:'Series'}), {1, 3}) self.assertEqual(cache.set_field('#series', {2:'Series [0]'}), {2}) cache2 = self.init_cache(cl) for c in (cache, cache2): for i, val in iteritems({1:'A Series One', 2:'A Series One', 3:'Series'}): self.assertEqual(c.field_for('series', i), val) cs_indices = {1:c.field_for('#series_index', 1), 3:c.field_for('#series_index', 3)} for i in (1, 2, 3): self.assertEqual(c.field_for('#series', i), 'Series') for i, val in iteritems({1:2, 2:1, 3:3}): self.assertEqual(c.field_for('series_index', i), val) for i, val in iteritems({1:cs_indices[1], 2:0, 3:cs_indices[3]}): self.assertEqual(c.field_for('#series_index', i), val) del cache2 # }}} def test_many_many_basic(self): # {{{ 'Test the different code paths for writing to a many-many field' cl = self.cloned_library cache = self.init_cache(cl) ae, af, sf = self.assertEqual, self.assertFalse, cache.set_field # Tags ae(sf('#tags', {1:cache.field_for('tags', 1), 2:cache.field_for('tags', 2)}), {1, 2}) for name in ('tags', '#tags'): f = cache.fields[name] af(sf(name, {1:('News', 'tag one')}, allow_case_change=False)) ae(sf(name, {1:'tag one, News'}), {1, 2}) ae(sf(name, {3:('tag two', 'sep,sep2')}), {2, 3}) ae(len(f.table.id_map), 4) ae(sf(name, {1:None}), {1}) cache2 = self.init_cache(cl) for c in (cache, cache2): ae(c.field_for(name, 3), ('tag two', 'sep;sep2')) ae(len(c.fields[name].table.id_map), 3) ae(len(c.fields[name].table.id_map), 3) ae(c.field_for(name, 1), ()) ae(c.field_for(name, 2), ('tag two', 'tag one')) del cache2 # Authors ae(sf('#authors', {k:cache.field_for('authors', k) for k in (1,2,3)}), {1,2,3}) for name in ('authors', '#authors'): f = cache.fields[name] ae(len(f.table.id_map), 3) af(cache.set_field(name, {3:'Unknown'})) ae(cache.set_field(name, {3:'Kovid Goyal & Divok Layog'}), {3}) ae(cache.set_field(name, {1:'', 2:'An, Author'}), {1,2}) cache2 = self.init_cache(cl) for c in (cache, cache2): ae(len(c.fields[name].table.id_map), 4 if name =='authors' else 3) ae(c.field_for(name, 3), ('Kovid Goyal', 'Divok Layog')) ae(c.field_for(name, 2), ('An, Author',)) ae(c.field_for(name, 1), (_('Unknown'),) if name=='authors' else ()) if name == 'authors': ae(c.field_for('author_sort', 1), author_to_author_sort(_('Unknown'))) ae(c.field_for('author_sort', 2), author_to_author_sort('An, Author')) ae(c.field_for('author_sort', 3), author_to_author_sort('Kovid Goyal') + ' & ' + author_to_author_sort('Divok Layog')) del cache2 ae(cache.set_field('authors', {1:'KoviD GoyaL'}), {1, 3}) ae(cache.field_for('author_sort', 1), 'GoyaL, KoviD') ae(cache.field_for('author_sort', 3), 'GoyaL, KoviD & Layog, Divok') # Languages f = cache.fields['languages'] ae(f.table.id_map, {1: 'eng', 2: 'deu'}) ae(sf('languages', {1:''}), {1}) ae(cache.field_for('languages', 1), ()) ae(sf('languages', {2:('und',)}), {2}) af(f.table.id_map) ae(sf('languages', {1:'eng,fra,deu', 2:'es,Dutch', 3:'English'}), {1, 2, 3}) ae(cache.field_for('languages', 1), ('eng', 'fra', 'deu')) ae(cache.field_for('languages', 2), ('spa', 'nld')) ae(cache.field_for('languages', 3), ('eng',)) ae(sf('languages', {3:None}), {3}) ae(cache.field_for('languages', 3), ()) ae(sf('languages', {1:'deu,fra,eng'}), {1}, 'Changing order failed') ae(sf('languages', {2:'deu,eng,eng'}), {2}) cache2 = self.init_cache(cl) for c in (cache, cache2): ae(cache.field_for('languages', 1), ('deu', 'fra', 'eng')) ae(cache.field_for('languages', 2), ('deu', 'eng')) del cache2 # Identifiers f = cache.fields['identifiers'] ae(sf('identifiers', {3: 'one:1,two:2'}), {3}) ae(sf('identifiers', {2:None}), {2}) ae(sf('identifiers', {1: {'test':'1', 'two':'2'}}), {1}) cache2 = self.init_cache(cl) for c in (cache, cache2): ae(c.field_for('identifiers', 3), {'one':'1', 'two':'2'}) ae(c.field_for('identifiers', 2), {}) ae(c.field_for('identifiers', 1), {'test':'1', 'two':'2'}) del cache2 # Test setting of title sort ae(sf('title', {1:'The Moose', 2:'Cat'}), {1, 2}) cache2 = self.init_cache(cl) for c in (cache, cache2): ae(c.field_for('sort', 1), title_sort('The Moose')) ae(c.field_for('sort', 2), title_sort('Cat')) # Test setting with the same value repeated ae(sf('tags', {3: ('a', 'b', 'a')}), {3}) ae(sf('tags', {3: ('x', 'X')}), {3}, 'Failed when setting tag twice with different cases') ae(('x',), cache.field_for('tags', 3)) # Test setting of authors with | in their names (for legacy database # format compatibility | is replaced by ,) ae(sf('authors', {3: ('Some| Author',)}), {3}) ae(('Some, Author',), cache.field_for('authors', 3)) # }}} def test_dirtied(self): # {{{ 'Test the setting of the dirtied flag and the last_modified column' cl = self.cloned_library cache = self.init_cache(cl) ae, af, sf = self.assertEqual, self.assertFalse, cache.set_field # First empty dirtied cache.dump_metadata() af(cache.dirtied_cache) af(self.init_cache(cl).dirtied_cache) prev = cache.field_for('last_modified', 3) import calibre.db.cache as c from datetime import timedelta utime = prev+timedelta(days=1) onowf = c.nowf c.nowf = lambda: utime try: ae(sf('title', {3:'xxx'}), {3}) self.assertTrue(3 in cache.dirtied_cache) ae(cache.field_for('last_modified', 3), utime) cache.dump_metadata() raw = cache.read_backup(3) from calibre.ebooks.metadata.opf2 import OPF opf = OPF(BytesIO(raw)) ae(opf.title, 'xxx') finally: c.nowf = onowf # }}} def test_backup(self): # {{{ 'Test the automatic backup of changed metadata' cl = self.cloned_library cache = self.init_cache(cl) ae, af, sf = self.assertEqual, self.assertFalse, cache.set_field # First empty dirtied cache.dump_metadata() af(cache.dirtied_cache) from calibre.db.backup import MetadataBackup interval = 0.01 mb = MetadataBackup(cache, interval=interval, scheduling_interval=0) mb.start() try: ae(sf('title', {1:'title1', 2:'title2', 3:'title3'}), {1,2,3}) ae(sf('authors', {1:'author1 & author2', 2:'author1 & author2', 3:'author1 & author2'}), {1,2,3}) count = 6 while cache.dirty_queue_length() and count > 0: mb.join(2) count -= 1 af(cache.dirty_queue_length()) finally: mb.stop() mb.join(2) af(mb.is_alive()) from calibre.ebooks.metadata.opf2 import OPF for book_id in (1, 2, 3): raw = cache.read_backup(book_id) opf = OPF(BytesIO(raw)) ae(opf.title, 'title%d'%book_id) ae(opf.authors, ['author1', 'author2']) # }}} def test_set_cover(self): # {{{ ' Test setting of cover ' cache = self.init_cache() ae = self.assertEqual # Test removing a cover ae(cache.field_for('cover', 1), 1) ae(cache.set_cover({1:None}), {1}) ae(cache.field_for('cover', 1), 0) img = IMG # Test setting a cover ae(cache.set_cover({bid:img for bid in (1, 2, 3)}), {1, 2, 3}) old = self.init_old() for book_id in (1, 2, 3): ae(cache.cover(book_id), img, 'Cover was not set correctly for book %d' % book_id) ae(cache.field_for('cover', book_id), 1) ae(old.cover(book_id, index_is_id=True), img, 'Cover was not set correctly for book %d' % book_id) self.assertTrue(old.has_cover(book_id)) old.close() old.break_cycles() del old # }}} def test_set_metadata(self): # {{{ ' Test setting of metadata ' ae = self.assertEqual cache = self.init_cache(self.cloned_library) # Check that changing title/author updates the path mi = cache.get_metadata(1) old_path = cache.field_for('path', 1) old_title, old_author = mi.title, mi.authors[0] ae(old_path, f'{old_author}/{old_title} (1)') mi.title, mi.authors = 'New Title', ['New Author'] cache.set_metadata(1, mi) ae(cache.field_for('path', 1), f'{mi.authors[0]}/{mi.title} (1)') p = cache.format_abspath(1, 'FMT1') self.assertTrue(mi.authors[0] in p and mi.title in p) # Compare old and new set_metadata() db = self.init_old(self.cloned_library) mi = db.get_metadata(1, index_is_id=True, get_cover=True, cover_as_data=True) mi2 = db.get_metadata(3, index_is_id=True, get_cover=True, cover_as_data=True) db.set_metadata(2, mi) db.set_metadata(1, mi2, force_changes=True) oldmi = db.get_metadata(2, index_is_id=True, get_cover=True, cover_as_data=True) oldmi2 = db.get_metadata(1, index_is_id=True, get_cover=True, cover_as_data=True) db.close() del db cache = self.init_cache(self.cloned_library) cache.set_metadata(2, mi) nmi = cache.get_metadata(2, get_cover=True, cover_as_data=True) ae(oldmi.cover_data, nmi.cover_data) self.compare_metadata(nmi, oldmi, exclude={'last_modified', 'format_metadata', 'formats'}) cache.set_metadata(1, mi2, force_changes=True) nmi2 = cache.get_metadata(1, get_cover=True, cover_as_data=True) self.compare_metadata(nmi2, oldmi2, exclude={'last_modified', 'format_metadata', 'formats'}) cache = self.init_cache(self.cloned_library) mi = cache.get_metadata(1) otags = mi.tags mi.tags = [x.upper() for x in mi.tags] cache.set_metadata(3, mi) self.assertEqual(set(otags), set(cache.field_for('tags', 3)), 'case changes should not be allowed in set_metadata') # test that setting authors without author sort results in an # auto-generated authors sort mi = Metadata('empty', ['a1', 'a2']) cache.set_metadata(1, mi) self.assertEqual('a1 & a2', cache.field_for('author_sort', 1)) cache.set_sort_for_authors({cache.get_item_id('authors', 'a1'): 'xy'}) self.assertEqual('xy & a2', cache.field_for('author_sort', 1)) mi = Metadata('empty', ['a1']) cache.set_metadata(1, mi) self.assertEqual('xy', cache.field_for('author_sort', 1)) # }}} def test_conversion_options(self): # {{{ ' Test saving of conversion options ' cache = self.init_cache() all_ids = cache.all_book_ids() self.assertFalse(cache.has_conversion_options(all_ids)) self.assertIsNone(cache.conversion_options(1)) op1, op2 = b"{'xx':'yy'}", b"{'yy':'zz'}" cache.set_conversion_options({1:op1, 2:op2}) self.assertTrue(cache.has_conversion_options(all_ids)) self.assertEqual(cache.conversion_options(1), op1) self.assertEqual(cache.conversion_options(2), op2) cache.set_conversion_options({1:op2}) self.assertEqual(cache.conversion_options(1), op2) cache.delete_conversion_options(all_ids) self.assertFalse(cache.has_conversion_options(all_ids)) # }}} def test_remove_items(self): # {{{ ' Test removal of many-(many,one) items ' cache = self.init_cache() tmap = cache.get_id_map('tags') self.assertEqual(cache.remove_items('tags', tmap), {1, 2}) tmap = cache.get_id_map('#tags') t = {v:k for k, v in iteritems(tmap)}['My Tag Two'] self.assertEqual(cache.remove_items('#tags', (t,)), {1, 2}) smap = cache.get_id_map('series') self.assertEqual(cache.remove_items('series', smap), {1, 2}) smap = cache.get_id_map('#series') s = {v:k for k, v in iteritems(smap)}['My Series Two'] self.assertEqual(cache.remove_items('#series', (s,)), {1}) for c in (cache, self.init_cache()): self.assertFalse(c.get_id_map('tags')) self.assertFalse(c.all_field_names('tags')) for bid in c.all_book_ids(): self.assertFalse(c.field_for('tags', bid)) self.assertEqual(len(c.get_id_map('#tags')), 1) self.assertEqual(c.all_field_names('#tags'), {'My Tag One'}) for bid in c.all_book_ids(): self.assertIn(c.field_for('#tags', bid), ((), ('My Tag One',))) for bid in (1, 2): self.assertEqual(c.field_for('series_index', bid), 1.0) self.assertFalse(c.get_id_map('series')) self.assertFalse(c.all_field_names('series')) for bid in c.all_book_ids(): self.assertFalse(c.field_for('series', bid)) self.assertEqual(c.field_for('series_index', 1), 1.0) self.assertEqual(c.all_field_names('#series'), {'My Series One'}) for bid in c.all_book_ids(): self.assertIn(c.field_for('#series', bid), (None, 'My Series One')) # Now test with restriction cache = self.init_cache() cache.set_field('tags', {1:'a,b,c', 2:'b,a', 3:'x,y,z'}) cache.set_field('series', {1:'a', 2:'a', 3:'b'}) cache.set_field('series_index', {1:8, 2:9, 3:3}) tmap, smap = cache.get_id_map('tags'), cache.get_id_map('series') self.assertEqual(cache.remove_items('tags', tmap, restrict_to_book_ids=()), set()) self.assertEqual(cache.remove_items('tags', tmap, restrict_to_book_ids={1}), {1}) self.assertEqual(cache.remove_items('series', smap, restrict_to_book_ids=()), set()) self.assertEqual(cache.remove_items('series', smap, restrict_to_book_ids=(1,)), {1}) c2 = self.init_cache() for c in (cache, c2): self.assertEqual(c.field_for('tags', 1), ()) self.assertEqual(c.field_for('tags', 2), ('b', 'a')) self.assertNotIn('c', set(itervalues(c.get_id_map('tags')))) self.assertEqual(c.field_for('series', 1), None) self.assertEqual(c.field_for('series', 2), 'a') self.assertEqual(c.field_for('series_index', 1), 1.0) self.assertEqual(c.field_for('series_index', 2), 9) # }}} def test_rename_items(self): # {{{ ' Test renaming of many-(many,one) items ' cl = self.cloned_library cache = self.init_cache(cl) # Check that renaming authors updates author sort and path a = {v:k for k, v in iteritems(cache.get_id_map('authors'))}['Unknown'] self.assertEqual(cache.rename_items('authors', {a:'New Author'})[0], {3}) a = {v:k for k, v in iteritems(cache.get_id_map('authors'))}['Author One'] self.assertEqual(cache.rename_items('authors', {a:'Author Two'})[0], {1, 2}) for c in (cache, self.init_cache(cl)): self.assertEqual(c.all_field_names('authors'), {'New Author', 'Author Two'}) self.assertEqual(c.field_for('author_sort', 3), 'Author, New') self.assertIn('New Author/', c.field_for('path', 3)) self.assertEqual(c.field_for('authors', 1), ('Author Two',)) self.assertEqual(c.field_for('author_sort', 1), 'Two, Author') t = {v:k for k, v in iteritems(cache.get_id_map('tags'))}['Tag One'] # Test case change self.assertEqual(cache.rename_items('tags', {t:'tag one'}), ({1, 2}, {t:t})) for c in (cache, self.init_cache(cl)): self.assertEqual(c.all_field_names('tags'), {'tag one', 'Tag Two', 'News'}) self.assertEqual(set(c.field_for('tags', 1)), {'tag one', 'News'}) self.assertEqual(set(c.field_for('tags', 2)), {'tag one', 'Tag Two'}) # Test new name self.assertEqual(cache.rename_items('tags', {t:'t1'})[0], {1,2}) for c in (cache, self.init_cache(cl)): self.assertEqual(c.all_field_names('tags'), {'t1', 'Tag Two', 'News'}) self.assertEqual(set(c.field_for('tags', 1)), {'t1', 'News'}) self.assertEqual(set(c.field_for('tags', 2)), {'t1', 'Tag Two'}) # Test rename to existing self.assertEqual(cache.rename_items('tags', {t:'Tag Two'})[0], {1,2}) for c in (cache, self.init_cache(cl)): self.assertEqual(c.all_field_names('tags'), {'Tag Two', 'News'}) self.assertEqual(set(c.field_for('tags', 1)), {'Tag Two', 'News'}) self.assertEqual(set(c.field_for('tags', 2)), {'Tag Two'}) # Test on a custom column t = {v:k for k, v in iteritems(cache.get_id_map('#tags'))}['My Tag One'] self.assertEqual(cache.rename_items('#tags', {t:'My Tag Two'})[0], {2}) for c in (cache, self.init_cache(cl)): self.assertEqual(c.all_field_names('#tags'), {'My Tag Two'}) self.assertEqual(set(c.field_for('#tags', 2)), {'My Tag Two'}) # Test a Many-one field s = {v:k for k, v in iteritems(cache.get_id_map('series'))}['A Series One'] # Test case change self.assertEqual(cache.rename_items('series', {s:'a series one'}), ({1, 2}, {s:s})) for c in (cache, self.init_cache(cl)): self.assertEqual(c.all_field_names('series'), {'a series one'}) self.assertEqual(c.field_for('series', 1), 'a series one') self.assertEqual(c.field_for('series_index', 1), 2.0) # Test new name self.assertEqual(cache.rename_items('series', {s:'series'})[0], {1, 2}) for c in (cache, self.init_cache(cl)): self.assertEqual(c.all_field_names('series'), {'series'}) self.assertEqual(c.field_for('series', 1), 'series') self.assertEqual(c.field_for('series', 2), 'series') self.assertEqual(c.field_for('series_index', 1), 2.0) s = {v:k for k, v in iteritems(cache.get_id_map('#series'))}['My Series One'] # Test custom column with rename to existing self.assertEqual(cache.rename_items('#series', {s:'My Series Two'})[0], {2}) for c in (cache, self.init_cache(cl)): self.assertEqual(c.all_field_names('#series'), {'My Series Two'}) self.assertEqual(c.field_for('#series', 2), 'My Series Two') self.assertEqual(c.field_for('#series_index', 1), 3.0) self.assertEqual(c.field_for('#series_index', 2), 4.0) # Test renaming many-many items to multiple items cache = self.init_cache(self.cloned_library) t = {v:k for k, v in iteritems(cache.get_id_map('tags'))}['Tag One'] affected_books, id_map = cache.rename_items('tags', {t:'Something, Else, Entirely'}) self.assertEqual({1, 2}, affected_books) tmap = cache.get_id_map('tags') self.assertEqual('Something', tmap[id_map[t]]) self.assertEqual(1, len(id_map)) f1, f2 = cache.field_for('tags', 1), cache.field_for('tags', 2) for f in (f1, f2): for t in 'Something,Else,Entirely'.split(','): self.assertIn(t, f) self.assertNotIn('Tag One', f) # Test with restriction cache = self.init_cache() cache.set_field('tags', {1:'a,b,c', 2:'x,y,z', 3:'a,x,z'}) tmap = {v:k for k, v in iteritems(cache.get_id_map('tags'))} self.assertEqual(cache.rename_items('tags', {tmap['a']:'r'}, restrict_to_book_ids=()), (set(), {})) self.assertEqual(cache.rename_items('tags', {tmap['a']:'r', tmap['b']:'q'}, restrict_to_book_ids=(1,))[0], {1}) self.assertEqual(cache.rename_items('tags', {tmap['x']:'X'}, restrict_to_book_ids=(2,))[0], {2}) c2 = self.init_cache() for c in (cache, c2): self.assertEqual(c.field_for('tags', 1), ('r', 'q', 'c')) self.assertEqual(c.field_for('tags', 2), ('X', 'y', 'z')) self.assertEqual(c.field_for('tags', 3), ('a', 'X', 'z')) # }}} def test_composite_cache(self): # {{{ ' Test that the composite field cache is properly invalidated on writes ' cache = self.init_cache() cache.create_custom_column('tc', 'TC', 'composite', False, display={ 'composite_template':'{title} {author_sort} {title_sort} {formats} {tags} {series} {series_index}'}) cache = self.init_cache() def test_invalidate(): c = self.init_cache() for bid in cache.all_book_ids(): self.assertEqual(cache.field_for('#tc', bid), c.field_for('#tc', bid)) cache.set_field('title', {1:'xx', 3:'yy'}) test_invalidate() cache.set_field('series_index', {1:9, 3:11}) test_invalidate() cache.rename_items('tags', {cache.get_item_id('tags', 'Tag One'):'xxx', cache.get_item_id('tags', 'News'):'news'}) test_invalidate() cache.remove_items('tags', (cache.get_item_id('tags', 'news'),)) test_invalidate() cache.set_sort_for_authors({cache.get_item_id('authors', 'Author One'):'meow'}) test_invalidate() cache.remove_formats({1:{'FMT1'}}) test_invalidate() cache.add_format(1, 'ADD', BytesIO(b'xxxx')) test_invalidate() # }}} def test_dump_and_restore(self): # {{{ ' Test roundtripping the db through SQL ' import warnings with warnings.catch_warnings(): # on python 3.10 apsw raises a deprecation warning which causes this test to fail on CI warnings.simplefilter('ignore', DeprecationWarning) cache = self.init_cache() uv = int(cache.backend.user_version) all_ids = cache.all_book_ids() cache.dump_and_restore() self.assertEqual(cache.set_field('title', {1:'nt'}), {1}, 'database connection broken') cache = self.init_cache() self.assertEqual(cache.all_book_ids(), all_ids, 'dump and restore broke database') self.assertEqual(int(cache.backend.user_version), uv) # }}} def test_set_author_data(self): # {{{ cache = self.init_cache() adata = cache.author_data() ldata = {aid:str(aid) for aid in adata} self.assertEqual({1,2,3}, cache.set_link_for_authors(ldata)) for c in (cache, self.init_cache()): self.assertEqual(ldata, {aid:d['link'] for aid, d in iteritems(c.author_data())}) self.assertEqual({3}, cache.set_link_for_authors({aid:'xxx' if aid == max(adata) else str(aid) for aid in adata}), 'Setting the author link to the same value as before, incorrectly marked some books as dirty') sdata = {aid:'%s, changed' % aid for aid in adata} self.assertEqual({1,2,3}, cache.set_sort_for_authors(sdata)) for bid in (1, 2, 3): self.assertIn(', changed', cache.field_for('author_sort', bid)) sdata = {aid:'%s, changed' % (aid*2 if aid == max(adata) else aid) for aid in adata} self.assertEqual({3}, cache.set_sort_for_authors(sdata), 'Setting the author sort to the same value as before, incorrectly marked some books as dirty') # }}} def test_fix_case_duplicates(self): # {{{ ' Test fixing of databases that have items in is_many fields that differ only by case ' ae = self.assertEqual cache = self.init_cache() conn = cache.backend.conn conn.execute('INSERT INTO publishers (name) VALUES ("mūs")') lid = conn.last_insert_rowid() conn.execute('INSERT INTO publishers (name) VALUES ("MŪS")') uid = conn.last_insert_rowid() conn.execute('DELETE FROM books_publishers_link') conn.execute('INSERT INTO books_publishers_link (book,publisher) VALUES (1, %d)' % lid) conn.execute('INSERT INTO books_publishers_link (book,publisher) VALUES (2, %d)' % uid) conn.execute('INSERT INTO books_publishers_link (book,publisher) VALUES (3, %d)' % uid) cache.reload_from_db() t = cache.fields['publisher'].table for x in (lid, uid): self.assertIn(x, t.id_map) self.assertIn(x, t.col_book_map) ae(t.book_col_map[1], lid) ae(t.book_col_map[2], uid) t.fix_case_duplicates(cache.backend) for c in (cache, self.init_cache()): t = c.fields['publisher'].table self.assertNotIn(uid, t.id_map) self.assertNotIn(uid, t.col_book_map) for bid in (1, 2, 3): ae(c.field_for('publisher', bid), "mūs") c.close() cache = self.init_cache() conn = cache.backend.conn conn.execute('INSERT INTO tags (name) VALUES ("mūūs")') lid = conn.last_insert_rowid() conn.execute('INSERT INTO tags (name) VALUES ("MŪŪS")') uid = conn.last_insert_rowid() conn.execute('INSERT INTO tags (name) VALUES ("mūŪS")') mid = conn.last_insert_rowid() conn.execute('INSERT INTO tags (name) VALUES ("t")') norm = conn.last_insert_rowid() conn.execute('DELETE FROM books_tags_link') for book_id, vals in iteritems({1:(lid, uid), 2:(uid, mid), 3:(lid, norm)}): conn.executemany('INSERT INTO books_tags_link (book,tag) VALUES (?,?)', tuple((book_id, x) for x in vals)) cache.reload_from_db() t = cache.fields['tags'].table for x in (lid, uid, mid): self.assertIn(x, t.id_map) self.assertIn(x, t.col_book_map) t.fix_case_duplicates(cache.backend) for c in (cache, self.init_cache()): t = c.fields['tags'].table for x in (uid, mid): self.assertNotIn(x, t.id_map) self.assertNotIn(x, t.col_book_map) ae(c.field_for('tags', 1), (t.id_map[lid],)) ae(c.field_for('tags', 2), (t.id_map[lid],), 'failed for book 2') ae(c.field_for('tags', 3), (t.id_map[lid], t.id_map[norm])) # }}} def test_preferences(self): # {{{ ' Test getting and setting of preferences, especially with mutable objects ' cache = self.init_cache() changes = [] cache.backend.conn.setupdatehook(lambda typ, dbname, tblname, rowid: changes.append(rowid)) prefs = cache.backend.prefs prefs['test mutable'] = [1, 2, 3] self.assertEqual(len(changes), 1) a = prefs['test mutable'] a.append(4) self.assertIn(4, prefs['test mutable']) prefs['test mutable'] = a self.assertEqual(len(changes), 2) prefs.load_from_db() self.assertIn(4, prefs['test mutable']) prefs['test mutable'] = {k:k for k in range(10)} self.assertEqual(len(changes), 3) prefs['test mutable'] = {k:k for k in reversed(range(10))} self.assertEqual(len(changes), 3, 'The database was written to despite there being no change in value') # }}} def test_annotations(self): # {{{ 'Test handling of annotations' from calibre.utils.date import utcnow, EPOCH cl = self.cloned_library cache = self.init_cache(cl) # First empty dirtied cache.dump_metadata() self.assertFalse(cache.dirtied_cache) def a(**kw): ts = utcnow() kw['timestamp'] = utcnow().isoformat() return kw, (ts - EPOCH).total_seconds() annot_list = [ a(type='bookmark', title='bookmark1 changed', seq=1), a(type='highlight', highlighted_text='text1', uuid='1', seq=2), a(type='highlight', highlighted_text='text2', uuid='2', seq=3, notes='notes2 some word changed again'), ] def map_as_list(amap): ans = [] for items in amap.values(): ans.extend(items) ans.sort(key=lambda x:x['seq']) return ans cache.set_annotations_for_book(1, 'moo', annot_list) amap = cache.annotations_map_for_book(1, 'moo') self.assertEqual(3, len(cache.all_annotations_for_book(1))) self.assertEqual([x[0] for x in annot_list], map_as_list(amap)) self.assertFalse(cache.dirtied_cache) cache.check_dirtied_annotations() self.assertEqual(set(cache.dirtied_cache), {1}) cache.dump_metadata() cache.check_dirtied_annotations() self.assertFalse(cache.dirtied_cache) # Test searching results = cache.search_annotations('"changed"') self.assertEqual([1, 3], [x['id'] for x in results]) results = cache.search_annotations('"changed"', annotation_type='bookmark') self.assertEqual([1], [x['id'] for x in results]) results = cache.search_annotations('"Changed"') # changed and change stem differently in english and other euro languages self.assertEqual([1, 3], [x['id'] for x in results]) results = cache.search_annotations('"SOMe"') self.assertEqual([3], [x['id'] for x in results]) results = cache.search_annotations('"change"', use_stemming=False) self.assertFalse(results) results = cache.search_annotations('"bookmark1"', highlight_start='[', highlight_end=']') self.assertEqual(results[0]['text'], '[bookmark1] changed') results = cache.search_annotations('"word"', highlight_start='[', highlight_end=']', snippet_size=3) self.assertEqual(results[0]['text'], '…some [word] changed…') self.assertRaises(FTSQueryError, cache.search_annotations, 'AND OR') fts_l = [a(type='bookmark', title='路坎坷走来', seq=1),] cache.set_annotations_for_book(1, 'moo', fts_l) results = cache.search_annotations('路', highlight_start='[', highlight_end=']') self.assertEqual(results[0]['text'], '[路]坎坷走来') annot_list[0][0]['title'] = 'changed title' cache.set_annotations_for_book(1, 'moo', annot_list) amap = cache.annotations_map_for_book(1, 'moo') self.assertEqual([x[0] for x in annot_list], map_as_list(amap)) del annot_list[1] cache.set_annotations_for_book(1, 'moo', annot_list) amap = cache.annotations_map_for_book(1, 'moo') self.assertEqual([x[0] for x in annot_list], map_as_list(amap)) cache.check_dirtied_annotations() cache.dump_metadata() from calibre.ebooks.metadata.opf2 import OPF raw = cache.read_backup(1) opf = OPF(BytesIO(raw)) cache.restore_annotations(1, list(opf.read_annotations())) amap = cache.annotations_map_for_book(1, 'moo') self.assertEqual([x[0] for x in annot_list], map_as_list(amap)) # }}} def test_changed_events(self): # {{{ def ae(l, r): # We need to sleep a bit to allow events to happen on its thread import time time.sleep(1) self.assertEqual(l, r) cache = self.init_cache(self.cloned_library) ae(cache.all_book_ids(), {1, 2, 3}) event_set = set() def event_func(t, library_id, *args): nonlocal event_set, ae event_set.update(args[0][1]) cache.add_listener(event_func) # Test that setting metadata to itself doesn't generate any events for id_ in cache.all_book_ids(): cache.set_metadata(id_, cache.get_metadata(id_)) ae(event_set, set()) # test setting a single field cache.set_field('tags', {1:'foo'}) ae(event_set, {1}) # test setting multiple books. Book 1 shouldn't get an event because it # isn't being changed event_set = set() cache.set_field('tags', {1:'foo', 2:'bar', 3:'mumble'}) ae(event_set, {2, 3}) # test setting a many-many field to empty event_set = set() cache.set_field('tags', {1:''}) ae(event_set, {1,}) event_set = set() cache.set_field('tags', {1:''}) ae(event_set, set()) # test setting title event_set = set() cache.set_field('title', {1:'Book 1'}) ae(event_set, {1}) ae(cache.field_for('title', 1), 'Book 1') # test setting series event_set = set() cache.set_field('series', {1:'GreatBooks [1]'}) cache.set_field('series', {2:'GreatBooks [0]'}) ae(event_set, {1,2}) ae(cache.field_for('series', 1), 'GreatBooks') ae(cache.field_for('series_index', 1), 1.0) ae(cache.field_for('series', 2), 'GreatBooks') ae(cache.field_for('series_index', 2), 0.0) # now series_index event_set = set() cache.set_field('series_index', {1:2}) ae(event_set, {1}) ae(cache.field_for('series_index', 1), 2.0) event_set = set() cache.set_field('series_index', {1:2, 2:3.5}) # book 1 isn't changed ae(event_set, {2}) ae(cache.field_for('series_index', 1), 2.0) ae(cache.field_for('series_index', 2), 3.5) # }}}