aboutsummaryrefslogtreecommitdiff
path: root/test/database.test.js
diff options
context:
space:
mode:
Diffstat (limited to 'test/database.test.js')
-rw-r--r--test/database.test.js853
1 files changed, 853 insertions, 0 deletions
diff --git a/test/database.test.js b/test/database.test.js
new file mode 100644
index 00000000..b53d0e65
--- /dev/null
+++ b/test/database.test.js
@@ -0,0 +1,853 @@
+/*
+ * Copyright (C) 2023 Yomitan Authors
+ * Copyright (C) 2020-2022 Yomichan Authors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+import {IDBFactory, IDBKeyRange} from 'fake-indexeddb';
+import path from 'path';
+import {beforeEach, describe, expect, test, vi} from 'vitest';
+import {createDictionaryArchive} from '../dev/util.js';
+import {DictionaryDatabase} from '../ext/js/language/dictionary-database.js';
+import {DictionaryImporterMediaLoader} from '../ext/js/language/dictionary-importer-media-loader.js';
+import {DictionaryImporter} from '../ext/js/language/dictionary-importer.js';
+
+vi.stubGlobal('IDBKeyRange', IDBKeyRange);
+
+vi.mock('../ext/js/language/dictionary-importer-media-loader.js');
+
+function createTestDictionaryArchive(dictionary, dictionaryName) {
+ const dictionaryDirectory = path.join(__dirname, 'data', 'dictionaries', dictionary);
+ return createDictionaryArchive(dictionaryDirectory, dictionaryName);
+}
+
+
+function createDictionaryImporter(onProgress) {
+ const dictionaryImporterMediaLoader = new DictionaryImporterMediaLoader();
+ return new DictionaryImporter(dictionaryImporterMediaLoader, (...args) => {
+ const {stepIndex, stepCount, index, count} = args[0];
+ expect(stepIndex < stepCount).toBe(true);
+ expect(index <= count).toBe(true);
+ if (typeof onProgress === 'function') {
+ onProgress(...args);
+ }
+ });
+}
+
+
+function countDictionaryDatabaseEntriesWithTerm(dictionaryDatabaseEntries, term) {
+ return dictionaryDatabaseEntries.reduce((i, v) => (i + (v.term === term ? 1 : 0)), 0);
+}
+
+function countDictionaryDatabaseEntriesWithReading(dictionaryDatabaseEntries, reading) {
+ return dictionaryDatabaseEntries.reduce((i, v) => (i + (v.reading === reading ? 1 : 0)), 0);
+}
+
+function countMetasWithMode(metas, mode) {
+ return metas.reduce((i, v) => (i + (v.mode === mode ? 1 : 0)), 0);
+}
+
+function countKanjiWithCharacter(kanji, character) {
+ return kanji.reduce((i, v) => (i + (v.character === character ? 1 : 0)), 0);
+}
+
+
+
+async function testDatabase1() {
+ test('Database1', async () => { // Load dictionary data
+ const testDictionary = createTestDictionaryArchive('valid-dictionary1');
+ const testDictionarySource = await testDictionary.generateAsync({type: 'arraybuffer'});
+ const testDictionaryIndex = JSON.parse(await testDictionary.files['index.json'].async('string'));
+
+ const title = testDictionaryIndex.title;
+ const titles = new Map([
+ [title, {priority: 0, allowSecondarySearches: false}]
+ ]);
+
+ // Setup iteration data
+ const iterations = [
+ {
+ cleanup: async () => {
+ // Test purge
+ await dictionaryDatabase.purge();
+ await testDatabaseEmpty1(dictionaryDatabase);
+ }
+ },
+ {
+ cleanup: async () => {
+ // Test deleteDictionary
+ let progressEvent = false;
+ await dictionaryDatabase.deleteDictionary(
+ title,
+ 1000,
+ () => {
+ progressEvent = true;
+ }
+ );
+ expect(progressEvent).toBe(true);
+
+ await testDatabaseEmpty1(dictionaryDatabase);
+ }
+ },
+ {
+ cleanup: async () => {}
+ }
+ ];
+
+ // Setup database
+ const dictionaryDatabase = new DictionaryDatabase();
+ await dictionaryDatabase.prepare();
+
+ for (const {cleanup} of iterations) {
+ const expectedSummary = {
+ title,
+ revision: 'test',
+ sequenced: true,
+ version: 3,
+ importDate: 0,
+ prefixWildcardsSupported: true,
+ counts: {
+ kanji: {total: 2},
+ kanjiMeta: {total: 6, freq: 6},
+ media: {total: 4},
+ tagMeta: {total: 15},
+ termMeta: {total: 38, freq: 31, pitch: 7},
+ terms: {total: 21}
+ }
+ };
+
+ // Import data
+ let progressEvent = false;
+ const dictionaryImporter = createDictionaryImporter(() => { progressEvent = true; });
+ const {result, errors} = await dictionaryImporter.importDictionary(
+ dictionaryDatabase,
+ testDictionarySource,
+ {prefixWildcardsSupported: true}
+ );
+ expectedSummary.importDate = result.importDate;
+ expect(errors).toStrictEqual([]);
+ expect(result).toStrictEqual(expectedSummary);
+ expect(progressEvent).toBe(true);
+
+ // Get info summary
+ const info = await dictionaryDatabase.getDictionaryInfo();
+ expect(info).toStrictEqual([expectedSummary]);
+
+ // Get counts
+ const counts = await dictionaryDatabase.getDictionaryCounts(
+ info.map((v) => v.title),
+ true
+ );
+ expect(counts).toStrictEqual({
+ counts: [{kanji: 2, kanjiMeta: 6, terms: 21, termMeta: 38, tagMeta: 15, media: 4}],
+ total: {kanji: 2, kanjiMeta: 6, terms: 21, termMeta: 38, tagMeta: 15, media: 4}
+ });
+
+ // Test find* functions
+ await testFindTermsBulkTest1(dictionaryDatabase, titles);
+ await testTindTermsExactBulk1(dictionaryDatabase, titles);
+ await testFindTermsBySequenceBulk1(dictionaryDatabase, title);
+ await testFindTermMetaBulk1(dictionaryDatabase, titles);
+ await testFindKanjiBulk1(dictionaryDatabase, titles);
+ await testFindKanjiMetaBulk1(dictionaryDatabase, titles);
+ await testFindTagForTitle1(dictionaryDatabase, title);
+
+ // Cleanup
+ await cleanup();
+ }
+
+ await dictionaryDatabase.close();
+ });
+}
+
+async function testDatabaseEmpty1(database) {
+ test('DatabaseEmpty1', async () => {
+ const info = await database.getDictionaryInfo();
+ expect(info).toStrictEqual([]);
+
+ const counts = await database.getDictionaryCounts([], true);
+ expect(counts).toStrictEqual({
+ counts: [],
+ total: {kanji: 0, kanjiMeta: 0, terms: 0, termMeta: 0, tagMeta: 0, media: 0}
+ });
+ });
+}
+
+async function testFindTermsBulkTest1(database, titles) {
+ test('FindTermsBulkTest1', async () => {
+ const data = [
+ {
+ inputs: [
+ {
+ matchType: null,
+ termList: ['打', '打つ', '打ち込む']
+ },
+ {
+ matchType: null,
+ termList: ['だ', 'ダース', 'うつ', 'ぶつ', 'うちこむ', 'ぶちこむ']
+ },
+ {
+ matchType: 'prefix',
+ termList: ['打']
+ }
+ ],
+ expectedResults: {
+ total: 10,
+ terms: [
+ ['打', 2],
+ ['打つ', 4],
+ ['打ち込む', 4]
+ ],
+ readings: [
+ ['だ', 1],
+ ['ダース', 1],
+ ['うつ', 2],
+ ['ぶつ', 2],
+ ['うちこむ', 2],
+ ['ぶちこむ', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ matchType: null,
+ termList: ['込む']
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ terms: [],
+ readings: []
+ }
+ },
+ {
+ inputs: [
+ {
+ matchType: 'suffix',
+ termList: ['込む']
+ }
+ ],
+ expectedResults: {
+ total: 4,
+ terms: [
+ ['打ち込む', 4]
+ ],
+ readings: [
+ ['うちこむ', 2],
+ ['ぶちこむ', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ matchType: null,
+ termList: []
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ terms: [],
+ readings: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {termList, matchType} of inputs) {
+ const results = await database.findTermsBulk(termList, titles, matchType);
+ expect(results.length).toStrictEqual(expectedResults.total);
+ for (const [term, count] of expectedResults.terms) {
+ expect(countDictionaryDatabaseEntriesWithTerm(results, term)).toStrictEqual(count);
+ }
+ for (const [reading, count] of expectedResults.readings) {
+ expect(countDictionaryDatabaseEntriesWithReading(results, reading)).toStrictEqual(count);
+ }
+ }
+ }
+ });
+}
+
+async function testTindTermsExactBulk1(database, titles) {
+ test('TindTermsExactBulk1', async () => {
+ const data = [
+ {
+ inputs: [
+ {
+ termList: [
+ {term: '打', reading: 'だ'},
+ {term: '打つ', reading: 'うつ'},
+ {term: '打ち込む', reading: 'うちこむ'}
+ ]
+ }
+ ],
+ expectedResults: {
+ total: 5,
+ terms: [
+ ['打', 1],
+ ['打つ', 2],
+ ['打ち込む', 2]
+ ],
+ readings: [
+ ['だ', 1],
+ ['うつ', 2],
+ ['うちこむ', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: [
+ {term: '打', reading: 'だ?'},
+ {term: '打つ', reading: 'うつ?'},
+ {term: '打ち込む', reading: 'うちこむ?'}
+ ]
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ terms: [],
+ readings: []
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: [
+ {term: '打つ', reading: 'うつ'},
+ {term: '打つ', reading: 'ぶつ'}
+ ]
+ }
+ ],
+ expectedResults: {
+ total: 4,
+ terms: [
+ ['打つ', 4]
+ ],
+ readings: [
+ ['うつ', 2],
+ ['ぶつ', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: [
+ {term: '打つ', reading: 'うちこむ'}
+ ]
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ terms: [],
+ readings: []
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: []
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ terms: [],
+ readings: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {termList} of inputs) {
+ const results = await database.findTermsExactBulk(termList, titles);
+ expect(results.length).toStrictEqual(expectedResults.total);
+ for (const [term, count] of expectedResults.terms) {
+ expect(countDictionaryDatabaseEntriesWithTerm(results, term)).toStrictEqual(count);
+ }
+ for (const [reading, count] of expectedResults.readings) {
+ expect(countDictionaryDatabaseEntriesWithReading(results, reading)).toStrictEqual(count);
+ }
+ }
+ }
+ });
+}
+
+async function testFindTermsBySequenceBulk1(database, mainDictionary) {
+ test('FindTermsBySequenceBulk1', async () => {
+ const data = [
+ {
+ inputs: [
+ {
+ sequenceList: [1, 2, 3, 4, 5]
+ }
+ ],
+ expectedResults: {
+ total: 11,
+ terms: [
+ ['打', 2],
+ ['打つ', 4],
+ ['打ち込む', 4],
+ ['画像', 1]
+ ],
+ readings: [
+ ['だ', 1],
+ ['ダース', 1],
+ ['うつ', 2],
+ ['ぶつ', 2],
+ ['うちこむ', 2],
+ ['ぶちこむ', 2],
+ ['がぞう', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [1]
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ terms: [
+ ['打', 1]
+ ],
+ readings: [
+ ['だ', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [2]
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ terms: [
+ ['打', 1]
+ ],
+ readings: [
+ ['ダース', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [3]
+ }
+ ],
+ expectedResults: {
+ total: 4,
+ terms: [
+ ['打つ', 4]
+ ],
+ readings: [
+ ['うつ', 2],
+ ['ぶつ', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [4]
+ }
+ ],
+ expectedResults: {
+ total: 4,
+ terms: [
+ ['打ち込む', 4]
+ ],
+ readings: [
+ ['うちこむ', 2],
+ ['ぶちこむ', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [5]
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ terms: [
+ ['画像', 1]
+ ],
+ readings: [
+ ['がぞう', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [-1]
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ terms: [],
+ readings: []
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: []
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ terms: [],
+ readings: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {sequenceList} of inputs) {
+ const results = await database.findTermsBySequenceBulk(sequenceList.map((query) => ({query, dictionary: mainDictionary})));
+ expect(results.length).toStrictEqual(expectedResults.total);
+ for (const [term, count] of expectedResults.terms) {
+ expect(countDictionaryDatabaseEntriesWithTerm(results, term)).toStrictEqual(count);
+ }
+ for (const [reading, count] of expectedResults.readings) {
+ expect(countDictionaryDatabaseEntriesWithReading(results, reading)).toStrictEqual(count);
+ }
+ }
+ }
+ });
+}
+
+async function testFindTermMetaBulk1(database, titles) {
+ test('FindTermMetaBulk1', async () => {
+ const data = [
+ {
+ inputs: [
+ {
+ termList: ['打']
+ }
+ ],
+ expectedResults: {
+ total: 11,
+ modes: [
+ ['freq', 11]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: ['打つ']
+ }
+ ],
+ expectedResults: {
+ total: 10,
+ modes: [
+ ['freq', 10]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: ['打ち込む']
+ }
+ ],
+ expectedResults: {
+ total: 12,
+ modes: [
+ ['freq', 10],
+ ['pitch', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: ['?']
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ modes: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {termList} of inputs) {
+ const results = await database.findTermMetaBulk(termList, titles);
+ expect(results.length).toStrictEqual(expectedResults.total);
+ for (const [mode, count] of expectedResults.modes) {
+ expect(countMetasWithMode(results, mode)).toStrictEqual(count);
+ }
+ }
+ }
+ });
+}
+
+async function testFindKanjiBulk1(database, titles) {
+ test('FindKanjiBulk1', async () => {
+ const data = [
+ {
+ inputs: [
+ {
+ kanjiList: ['打']
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ kanji: [
+ ['打', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ kanjiList: ['込']
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ kanji: [
+ ['込', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ kanjiList: ['?']
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ kanji: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {kanjiList} of inputs) {
+ const results = await database.findKanjiBulk(kanjiList, titles);
+ expect(results.length).toStrictEqual(expectedResults.total);
+ for (const [kanji, count] of expectedResults.kanji) {
+ expect(countKanjiWithCharacter(results, kanji)).toStrictEqual(count);
+ }
+ }
+ }
+ });
+}
+
+async function testFindKanjiMetaBulk1(database, titles) {
+ test('FindKanjiMetaBulk1', async () => {
+ const data = [
+ {
+ inputs: [
+ {
+ kanjiList: ['打']
+ }
+ ],
+ expectedResults: {
+ total: 3,
+ modes: [
+ ['freq', 3]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ kanjiList: ['込']
+ }
+ ],
+ expectedResults: {
+ total: 3,
+ modes: [
+ ['freq', 3]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ kanjiList: ['?']
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ modes: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {kanjiList} of inputs) {
+ const results = await database.findKanjiMetaBulk(kanjiList, titles);
+ expect(results.length).toStrictEqual(expectedResults.total);
+ for (const [mode, count] of expectedResults.modes) {
+ expect(countMetasWithMode(results, mode)).toStrictEqual(count);
+ }
+ }
+ }
+ });
+}
+
+async function testFindTagForTitle1(database, title) {
+ test('FindTagForTitle1', async () => {
+ const data = [
+ {
+ inputs: [
+ {
+ name: 'E1'
+ }
+ ],
+ expectedResults: {
+ value: {category: 'default', dictionary: title, name: 'E1', notes: 'example tag 1', order: 0, score: 0}
+ }
+ },
+ {
+ inputs: [
+ {
+ name: 'K1'
+ }
+ ],
+ expectedResults: {
+ value: {category: 'default', dictionary: title, name: 'K1', notes: 'example kanji tag 1', order: 0, score: 0}
+ }
+ },
+ {
+ inputs: [
+ {
+ name: 'kstat1'
+ }
+ ],
+ expectedResults: {
+ value: {category: 'class', dictionary: title, name: 'kstat1', notes: 'kanji stat 1', order: 0, score: 0}
+ }
+ },
+ {
+ inputs: [
+ {
+ name: 'invalid'
+ }
+ ],
+ expectedResults: {
+ value: null
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {name} of inputs) {
+ const result = await database.findTagForTitle(name, title);
+ expect(result).toStrictEqual(expectedResults.value);
+ }
+ }
+ });
+}
+
+
+async function testDatabase2() {
+ test('Database2', async () => { // Load dictionary data
+ const testDictionary = createTestDictionaryArchive('valid-dictionary1');
+ const testDictionarySource = await testDictionary.generateAsync({type: 'arraybuffer'});
+ const testDictionaryIndex = JSON.parse(await testDictionary.files['index.json'].async('string'));
+
+ const title = testDictionaryIndex.title;
+ const titles = new Map([
+ [title, {priority: 0, allowSecondarySearches: false}]
+ ]);
+
+ // Setup database
+ const dictionaryDatabase = new DictionaryDatabase();
+
+ // Database not open
+ await expect(dictionaryDatabase.deleteDictionary(title, 1000)).rejects.toThrow('Database not open');
+ await expect(dictionaryDatabase.findTermsBulk(['?'], titles, null)).rejects.toThrow('Database not open');
+ await expect(dictionaryDatabase.findTermsExactBulk([{term: '?', reading: '?'}], titles)).rejects.toThrow('Database not open');
+ await expect(dictionaryDatabase.findTermsBySequenceBulk([{query: 1, dictionary: title}])).rejects.toThrow('Database not open');
+ await expect(dictionaryDatabase.findTermMetaBulk(['?'], titles)).rejects.toThrow('Database not open');
+ await expect(dictionaryDatabase.findTermMetaBulk(['?'], titles)).rejects.toThrow('Database not open');
+ await expect(dictionaryDatabase.findKanjiBulk(['?'], titles)).rejects.toThrow('Database not open');
+ await expect(dictionaryDatabase.findKanjiMetaBulk(['?'], titles)).rejects.toThrow('Database not open');
+ await expect(dictionaryDatabase.findTagForTitle('tag', title)).rejects.toThrow('Database not open');
+ await expect(dictionaryDatabase.getDictionaryInfo()).rejects.toThrow('Database not open');
+ await expect(dictionaryDatabase.getDictionaryCounts(titles, true)).rejects.toThrow('Database not open');
+ await expect(createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, {})).rejects.toThrow('Database is not ready');
+
+ await dictionaryDatabase.prepare();
+
+ // already prepared
+ await expect(dictionaryDatabase.prepare()).rejects.toThrow('Database already open');
+
+ await createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, {});
+
+ // dictionary already imported
+ await expect(createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, {})).rejects.toThrow('Dictionary is already imported');
+
+ await dictionaryDatabase.close();
+ });
+}
+
+
+async function testDatabase3() {
+ const invalidDictionaries = [
+ 'invalid-dictionary1',
+ 'invalid-dictionary2',
+ 'invalid-dictionary3',
+ 'invalid-dictionary4',
+ 'invalid-dictionary5',
+ 'invalid-dictionary6'
+ ];
+
+
+ describe('Database3', () => {
+ for (const invalidDictionary of invalidDictionaries) {
+ test(`${invalidDictionary}`, async () => {
+ // Setup database
+ const dictionaryDatabase = new DictionaryDatabase();
+ await dictionaryDatabase.prepare();
+
+ const testDictionary = createTestDictionaryArchive(invalidDictionary);
+ const testDictionarySource = await testDictionary.generateAsync({type: 'arraybuffer'});
+
+ await expect(createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, {})).rejects.toThrow('Dictionary has invalid data');
+ await dictionaryDatabase.close();
+ });
+ }
+ });
+}
+
+
+async function main() {
+ beforeEach(async () => {
+ globalThis.indexedDB = new IDBFactory();
+ });
+ await testDatabase1();
+ await testDatabase2();
+ await testDatabase3();
+}
+
+await main();