summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2020-02-22 14:34:20 -0500
committerGitHub <noreply@github.com>2020-02-22 14:34:20 -0500
commitf3c4b0e1e14cbdfb86c692e89144c762801b2339 (patch)
treee7e77fca7b151c1f9a17ab5a1314589a5578dd28
parent0e31139734efd51799efcb6357c074059c78858d (diff)
parenta2b72dd3ab4a560d4549aa52912fcb9cd9f275ab (diff)
Merge pull request #369 from toasted-nutbread/dictionary-validation
Dictionary validation
-rw-r--r--ext/bg/data/dictionary-index-schema.json33
-rw-r--r--ext/bg/js/database.js581
-rw-r--r--test/data/dictionaries/invalid-dictionary1/index.json7
-rw-r--r--test/data/dictionaries/invalid-dictionary2/index.json7
-rw-r--r--test/data/dictionaries/invalid-dictionary2/kanji_bank_1.json3
-rw-r--r--test/data/dictionaries/invalid-dictionary3/index.json7
-rw-r--r--test/data/dictionaries/invalid-dictionary3/kanji_meta_bank_1.json1
-rw-r--r--test/data/dictionaries/invalid-dictionary4/index.json7
-rw-r--r--test/data/dictionaries/invalid-dictionary4/tag_bank_1.json3
-rw-r--r--test/data/dictionaries/invalid-dictionary5/index.json7
-rw-r--r--test/data/dictionaries/invalid-dictionary5/term_bank_1.json3
-rw-r--r--test/data/dictionaries/invalid-dictionary6/index.json7
-rw-r--r--test/data/dictionaries/invalid-dictionary6/term_meta_bank_1.json1
-rw-r--r--test/data/dictionaries/valid-dictionary1/index.json (renamed from test/test-dictionary-data/index.json)0
-rw-r--r--test/data/dictionaries/valid-dictionary1/kanji_bank_1.json (renamed from test/test-dictionary-data/kanji_bank_1.json)0
-rw-r--r--test/data/dictionaries/valid-dictionary1/kanji_meta_bank_1.json (renamed from test/test-dictionary-data/kanji_meta_bank_1.json)0
-rw-r--r--test/data/dictionaries/valid-dictionary1/tag_bank_1.json (renamed from test/test-dictionary-data/tag_bank_1.json)0
-rw-r--r--test/data/dictionaries/valid-dictionary1/tag_bank_2.json (renamed from test/test-dictionary-data/tag_bank_2.json)0
-rw-r--r--test/data/dictionaries/valid-dictionary1/term_bank_1.json (renamed from test/test-dictionary-data/term_bank_1.json)0
-rw-r--r--test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json (renamed from test/test-dictionary-data/term_meta_bank_1.json)0
-rw-r--r--test/test-database.js177
-rw-r--r--test/test-dictionary.js33
-rw-r--r--test/yomichan-test.js15
23 files changed, 555 insertions, 337 deletions
diff --git a/ext/bg/data/dictionary-index-schema.json b/ext/bg/data/dictionary-index-schema.json
index 9865fcc1..9311f14c 100644
--- a/ext/bg/data/dictionary-index-schema.json
+++ b/ext/bg/data/dictionary-index-schema.json
@@ -22,11 +22,40 @@
},
"format": {
"type": "integer",
- "description": "Format of data found in the JSON data files."
+ "description": "Format of data found in the JSON data files.",
+ "enum": [1, 2, 3]
},
"version": {
"type": "integer",
- "description": "Alias for format."
+ "description": "Alias for format.",
+ "enum": [1, 2, 3]
+ },
+ "tagMeta": {
+ "type": "object",
+ "description": "Tag information for terms and kanji. This object is obsolete and individual tag files should be used instead.",
+ "additionalProperties": {
+ "type": "object",
+ "description": "Information about a single tag. The object key is the name of the tag.",
+ "properties": {
+ "category": {
+ "type": "string",
+ "description": "Category for the tag."
+ },
+ "order": {
+ "type": "number",
+ "description": "Sorting order for the tag."
+ },
+ "notes": {
+ "type": "string",
+ "description": "Notes for the tag."
+ },
+ "score": {
+ "type": "number",
+ "description": "Score used to determine popularity. Negative values are more rare and positive values are more frequent. This score is also used to sort search results."
+ }
+ },
+ "additionalProperties": false
+ }
}
},
"anyOf": [
diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js
index 02d59c83..453cf15f 100644
--- a/ext/bg/js/database.js
+++ b/ext/bg/js/database.js
@@ -16,21 +16,24 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/*global dictFieldSplit, dictTagSanitize, JSZip*/
+/*global dictFieldSplit, requestJson, JsonSchema, JSZip*/
class Database {
constructor() {
this.db = null;
+ this._schemas = new Map();
}
+ // Public
+
async prepare() {
if (this.db !== null) {
throw new Error('Database already initialized');
}
try {
- this.db = await Database.open('dict', 5, (db, transaction, oldVersion) => {
- Database.upgrade(db, transaction, oldVersion, [
+ this.db = await Database._open('dict', 5, (db, transaction, oldVersion) => {
+ Database._upgrade(db, transaction, oldVersion, [
{
version: 2,
stores: {
@@ -97,23 +100,23 @@ class Database {
}
async close() {
- this.validate();
+ this._validate();
this.db.close();
this.db = null;
}
async purge() {
- this.validate();
+ this._validate();
this.db.close();
- await Database.deleteDatabase(this.db.name);
+ await Database._deleteDatabase(this.db.name);
this.db = null;
await this.prepare();
}
async deleteDictionary(dictionaryName, onProgress, progressSettings) {
- this.validate();
+ this._validate();
const targets = [
['dictionaries', 'title'],
@@ -140,14 +143,14 @@ class Database {
const dbObjectStore = dbTransaction.objectStore(objectStoreName);
const dbIndex = dbObjectStore.index(index);
const only = IDBKeyRange.only(dictionaryName);
- promises.push(Database.deleteValues(dbObjectStore, dbIndex, only, onProgress, progressData, progressRate));
+ promises.push(Database._deleteValues(dbObjectStore, dbIndex, only, onProgress, progressData, progressRate));
}
await Promise.all(promises);
}
async findTermsBulk(termList, titles, wildcard) {
- this.validate();
+ this._validate();
const promises = [];
const visited = {};
@@ -155,7 +158,7 @@ class Database {
const processRow = (row, index) => {
if (titles.includes(row.dictionary) && !hasOwn(visited, row.id)) {
visited[row.id] = true;
- results.push(Database.createTerm(row, index));
+ results.push(Database._createTerm(row, index));
}
};
@@ -171,8 +174,8 @@ class Database {
const term = prefixWildcard ? stringReverse(termList[i]) : termList[i];
const query = useWildcard ? IDBKeyRange.bound(term, `${term}\uffff`, false, false) : IDBKeyRange.only(term);
promises.push(
- Database.getAll(dbIndex1, query, i, processRow),
- Database.getAll(dbIndex2, query, i, processRow)
+ Database._getAll(dbIndex1, query, i, processRow),
+ Database._getAll(dbIndex2, query, i, processRow)
);
}
@@ -182,13 +185,13 @@ class Database {
}
async findTermsExactBulk(termList, readingList, titles) {
- this.validate();
+ this._validate();
const promises = [];
const results = [];
const processRow = (row, index) => {
if (row.reading === readingList[index] && titles.includes(row.dictionary)) {
- results.push(Database.createTerm(row, index));
+ results.push(Database._createTerm(row, index));
}
};
@@ -198,7 +201,7 @@ class Database {
for (let i = 0; i < termList.length; ++i) {
const only = IDBKeyRange.only(termList[i]);
- promises.push(Database.getAll(dbIndex, only, i, processRow));
+ promises.push(Database._getAll(dbIndex, only, i, processRow));
}
await Promise.all(promises);
@@ -207,13 +210,13 @@ class Database {
}
async findTermsBySequenceBulk(sequenceList, mainDictionary) {
- this.validate();
+ this._validate();
const promises = [];
const results = [];
const processRow = (row, index) => {
if (row.dictionary === mainDictionary) {
- results.push(Database.createTerm(row, index));
+ results.push(Database._createTerm(row, index));
}
};
@@ -223,7 +226,7 @@ class Database {
for (let i = 0; i < sequenceList.length; ++i) {
const only = IDBKeyRange.only(sequenceList[i]);
- promises.push(Database.getAll(dbIndex, only, i, processRow));
+ promises.push(Database._getAll(dbIndex, only, i, processRow));
}
await Promise.all(promises);
@@ -232,51 +235,26 @@ class Database {
}
async findTermMetaBulk(termList, titles) {
- return this.findGenericBulk('termMeta', 'expression', termList, titles, Database.createTermMeta);
+ return this._findGenericBulk('termMeta', 'expression', termList, titles, Database._createTermMeta);
}
async findKanjiBulk(kanjiList, titles) {
- return this.findGenericBulk('kanji', 'character', kanjiList, titles, Database.createKanji);
+ return this._findGenericBulk('kanji', 'character', kanjiList, titles, Database._createKanji);
}
async findKanjiMetaBulk(kanjiList, titles) {
- return this.findGenericBulk('kanjiMeta', 'character', kanjiList, titles, Database.createKanjiMeta);
- }
-
- async findGenericBulk(tableName, indexName, indexValueList, titles, createResult) {
- this.validate();
-
- const promises = [];
- const results = [];
- const processRow = (row, index) => {
- if (titles.includes(row.dictionary)) {
- results.push(createResult(row, index));
- }
- };
-
- const dbTransaction = this.db.transaction([tableName], 'readonly');
- const dbTerms = dbTransaction.objectStore(tableName);
- const dbIndex = dbTerms.index(indexName);
-
- for (let i = 0; i < indexValueList.length; ++i) {
- const only = IDBKeyRange.only(indexValueList[i]);
- promises.push(Database.getAll(dbIndex, only, i, processRow));
- }
-
- await Promise.all(promises);
-
- return results;
+ return this._findGenericBulk('kanjiMeta', 'character', kanjiList, titles, Database._createKanjiMeta);
}
async findTagForTitle(name, title) {
- this.validate();
+ this._validate();
let result = null;
const dbTransaction = this.db.transaction(['tagMeta'], 'readonly');
const dbTerms = dbTransaction.objectStore('tagMeta');
const dbIndex = dbTerms.index('name');
const only = IDBKeyRange.only(name);
- await Database.getAll(dbIndex, only, null, (row) => {
+ await Database._getAll(dbIndex, only, null, (row) => {
if (title === row.dictionary) {
result = row;
}
@@ -286,19 +264,19 @@ class Database {
}
async getDictionaryInfo() {
- this.validate();
+ this._validate();
const results = [];
const dbTransaction = this.db.transaction(['dictionaries'], 'readonly');
const dbDictionaries = dbTransaction.objectStore('dictionaries');
- await Database.getAll(dbDictionaries, null, null, (info) => results.push(info));
+ await Database._getAll(dbDictionaries, null, null, (info) => results.push(info));
return results;
}
async getDictionaryCounts(dictionaryNames, getTotal) {
- this.validate();
+ this._validate();
const objectStoreNames = [
'kanji',
@@ -319,7 +297,7 @@ class Database {
// Query is required for Edge, otherwise index.count throws an exception.
const query1 = IDBKeyRange.lowerBound('', false);
- const totalPromise = getTotal ? Database.getCounts(targets, query1) : null;
+ const totalPromise = getTotal ? Database._getCounts(targets, query1) : null;
const counts = [];
const countPromises = [];
@@ -327,7 +305,7 @@ class Database {
counts.push(null);
const index = i;
const query2 = IDBKeyRange.only(dictionaryNames[i]);
- const countPromise = Database.getCounts(targets, query2).then((v) => counts[index] = v);
+ const countPromise = Database._getCounts(targets, query2).then((v) => counts[index] = v);
countPromises.push(countPromise);
}
await Promise.all(countPromises);
@@ -339,278 +317,287 @@ class Database {
return result;
}
- async importDictionary(archive, progressCallback, details) {
- this.validate();
+ async importDictionary(archiveSource, onProgress, details) {
+ this._validate();
+ const db = this.db;
+ const hasOnProgress = (typeof onProgress === 'function');
- const errors = [];
- const prefixWildcardsSupported = details.prefixWildcardsSupported;
+ // Read archive
+ const archive = await JSZip.loadAsync(archiveSource);
- const maxTransactionLength = 1000;
- const bulkAdd = async (objectStoreName, items, total, current) => {
- const db = this.db;
- for (let i = 0; i < items.length; i += maxTransactionLength) {
- if (progressCallback) {
- progressCallback(total, current + i / items.length);
- }
+ // Read and validate index
+ const indexFileName = 'index.json';
+ const indexFile = archive.files[indexFileName];
+ if (!indexFile) {
+ throw new Error('No dictionary index found in archive');
+ }
- try {
- const count = Math.min(maxTransactionLength, items.length - i);
- const transaction = db.transaction([objectStoreName], 'readwrite');
- const objectStore = transaction.objectStore(objectStoreName);
- await Database.bulkAdd(objectStore, items, i, count);
- } catch (e) {
- errors.push(e);
- }
- }
- };
+ const index = JSON.parse(await indexFile.async('string'));
- const indexDataLoaded = async (summary) => {
- if (summary.version > 3) {
- throw new Error('Unsupported dictionary version');
- }
+ const indexSchema = await this._getSchema('/bg/data/dictionary-index-schema.json');
+ Database._validateJsonSchema(index, indexSchema, indexFileName);
- const db = this.db;
- const dbCountTransaction = db.transaction(['dictionaries'], 'readonly');
- const dbIndex = dbCountTransaction.objectStore('dictionaries').index('title');
- const only = IDBKeyRange.only(summary.title);
- const count = await Database.getCount(dbIndex, only);
+ const dictionaryTitle = index.title;
+ const version = index.format || index.version;
- if (count > 0) {
- throw new Error('Dictionary is already imported');
- }
+ if (!dictionaryTitle || !index.revision) {
+ throw new Error('Unrecognized dictionary format');
+ }
- const transaction = db.transaction(['dictionaries'], 'readwrite');
- const objectStore = transaction.objectStore('dictionaries');
- await Database.bulkAdd(objectStore, [summary], 0, 1);
- };
+ // Verify database is not already imported
+ if (await this._dictionaryExists(dictionaryTitle)) {
+ throw new Error('Dictionary is already imported');
+ }
- const termDataLoaded = async (summary, entries, total, current) => {
- const rows = [];
- if (summary.version === 1) {
- for (const [expression, reading, definitionTags, rules, score, ...glossary] of entries) {
- rows.push({
- expression,
- reading,
- definitionTags,
- rules,
- score,
- glossary,
- dictionary: summary.title
- });
- }
+ // Data format converters
+ const convertTermBankEntry = (entry) => {
+ if (version === 1) {
+ const [expression, reading, definitionTags, rules, score, ...glossary] = entry;
+ return {expression, reading, definitionTags, rules, score, glossary};
} else {
- for (const [expression, reading, definitionTags, rules, score, glossary, sequence, termTags] of entries) {
- rows.push({
- expression,
- reading,
- definitionTags,
- rules,
- score,
- glossary,
- sequence,
- termTags,
- dictionary: summary.title
- });
- }
- }
-
- if (prefixWildcardsSupported) {
- for (const row of rows) {
- row.expressionReverse = stringReverse(row.expression);
- row.readingReverse = stringReverse(row.reading);
- }
+ const [expression, reading, definitionTags, rules, score, glossary, sequence, termTags] = entry;
+ return {expression, reading, definitionTags, rules, score, glossary, sequence, termTags};
}
+ };
- await bulkAdd('terms', rows, total, current);
+ const convertTermMetaBankEntry = (entry) => {
+ const [expression, mode, data] = entry;
+ return {expression, mode, data};
};
- const termMetaDataLoaded = async (summary, entries, total, current) => {
- const rows = [];
- for (const [expression, mode, data] of entries) {
- rows.push({
- expression,
- mode,
- data,
- dictionary: summary.title
- });
+ const convertKanjiBankEntry = (entry) => {
+ if (version === 1) {
+ const [character, onyomi, kunyomi, tags, ...meanings] = entry;
+ return {character, onyomi, kunyomi, tags, meanings};
+ } else {
+ const [character, onyomi, kunyomi, tags, meanings, stats] = entry;
+ return {character, onyomi, kunyomi, tags, meanings, stats};
}
+ };
- await bulkAdd('termMeta', rows, total, current);
+ const convertKanjiMetaBankEntry = (entry) => {
+ const [character, mode, data] = entry;
+ return {character, mode, data};
};
- const kanjiDataLoaded = async (summary, entries, total, current) => {
- const rows = [];
- if (summary.version === 1) {
- for (const [character, onyomi, kunyomi, tags, ...meanings] of entries) {
- rows.push({
- character,
- onyomi,
- kunyomi,
- tags,
- meanings,
- dictionary: summary.title
- });
- }
- } else {
- for (const [character, onyomi, kunyomi, tags, meanings, stats] of entries) {
- rows.push({
- character,
- onyomi,
- kunyomi,
- tags,
- meanings,
- stats,
- dictionary: summary.title
- });
+ const convertTagBankEntry = (entry) => {
+ const [name, category, order, notes, score] = entry;
+ return {name, category, order, notes, score};
+ };
+
+ // Archive file reading
+ const readFileSequence = async (fileNameFormat, convertEntry, schema) => {
+ const results = [];
+ for (let i = 1; true; ++i) {
+ const fileName = fileNameFormat.replace(/\?/, `${i}`);
+ const file = archive.files[fileName];
+ if (!file) { break; }
+
+ const entries = JSON.parse(await file.async('string'));
+ Database._validateJsonSchema(entries, schema, fileName);
+
+ for (let entry of entries) {
+ entry = convertEntry(entry);
+ entry.dictionary = dictionaryTitle;
+ results.push(entry);
}
}
-
- await bulkAdd('kanji', rows, total, current);
+ return results;
};
- const kanjiMetaDataLoaded = async (summary, entries, total, current) => {
- const rows = [];
- for (const [character, mode, data] of entries) {
- rows.push({
- character,
- mode,
- data,
- dictionary: summary.title
- });
+ // Load schemas
+ const dataBankSchemaPaths = this.constructor._getDataBankSchemaPaths(version);
+ const dataBankSchemas = await Promise.all(dataBankSchemaPaths.map((path) => this._getSchema(path)));
+
+ // Load data
+ const termList = await readFileSequence('term_bank_?.json', convertTermBankEntry, dataBankSchemas[0]);
+ const termMetaList = await readFileSequence('term_meta_bank_?.json', convertTermMetaBankEntry, dataBankSchemas[1]);
+ const kanjiList = await readFileSequence('kanji_bank_?.json', convertKanjiBankEntry, dataBankSchemas[2]);
+ const kanjiMetaList = await readFileSequence('kanji_meta_bank_?.json', convertKanjiMetaBankEntry, dataBankSchemas[3]);
+ const tagList = await readFileSequence('tag_bank_?.json', convertTagBankEntry, dataBankSchemas[4]);
+
+ // Old tags
+ const indexTagMeta = index.tagMeta;
+ if (typeof indexTagMeta === 'object' && indexTagMeta !== null) {
+ for (const name of Object.keys(indexTagMeta)) {
+ const {category, order, notes, score} = indexTagMeta[name];
+ tagList.push({name, category, order, notes, score});
}
+ }
- await bulkAdd('kanjiMeta', rows, total, current);
- };
-
- const tagDataLoaded = async (summary, entries, total, current) => {
- const rows = [];
- for (const [name, category, order, notes, score] of entries) {
- const row = dictTagSanitize({
- name,
- category,
- order,
- notes,
- score,
- dictionary: summary.title
- });
-
- rows.push(row);
+ // Prefix wildcard support
+ const prefixWildcardsSupported = !!details.prefixWildcardsSupported;
+ if (prefixWildcardsSupported) {
+ for (const entry of termList) {
+ entry.expressionReverse = stringReverse(entry.expression);
+ entry.readingReverse = stringReverse(entry.reading);
}
+ }
- await bulkAdd('tagMeta', rows, total, current);
+ // Add dictionary
+ const summary = {
+ title: dictionaryTitle,
+ revision: index.revision,
+ sequenced: index.sequenced,
+ version,
+ prefixWildcardsSupported
};
- const result = await Database.importDictionaryZip(
- archive,
- indexDataLoaded,
- termDataLoaded,
- termMetaDataLoaded,
- kanjiDataLoaded,
- kanjiMetaDataLoaded,
- tagDataLoaded,
- details
+ {
+ const transaction = db.transaction(['dictionaries'], 'readwrite');
+ const objectStore = transaction.objectStore('dictionaries');
+ await Database._bulkAdd(objectStore, [summary], 0, 1);
+ }
+
+ // Add data
+ const errors = [];
+ const total = (
+ termList.length +
+ termMetaList.length +
+ kanjiList.length +
+ kanjiMetaList.length +
+ tagList.length
);
+ let loadedCount = 0;
+ const maxTransactionLength = 1000;
+
+ const bulkAdd = async (objectStoreName, entries) => {
+ const ii = entries.length;
+ for (let i = 0; i < ii; i += maxTransactionLength) {
+ const count = Math.min(maxTransactionLength, ii - i);
+
+ try {
+ const transaction = db.transaction([objectStoreName], 'readwrite');
+ const objectStore = transaction.objectStore(objectStoreName);
+ await Database._bulkAdd(objectStore, entries, i, count);
+ } catch (e) {
+ errors.push(e);
+ }
+
+ loadedCount += count;
+ if (hasOnProgress) {
+ onProgress(total, loadedCount);
+ }
+ }
+ };
+
+ await bulkAdd('terms', termList);
+ await bulkAdd('termMeta', termMetaList);
+ await bulkAdd('kanji', kanjiList);
+ await bulkAdd('kanjiMeta', kanjiMetaList);
+ await bulkAdd('tagMeta', tagList);
- return {result, errors};
+ return {result: summary, errors};
}
- validate() {
+ // Private
+
+ _validate() {
if (this.db === null) {
throw new Error('Database not initialized');
}
}
- static async importDictionaryZip(
- archive,
- indexDataLoaded,
- termDataLoaded,
- termMetaDataLoaded,
- kanjiDataLoaded,
- kanjiMetaDataLoaded,
- tagDataLoaded,
- details
- ) {
- const zip = await JSZip.loadAsync(archive);
-
- const indexFile = zip.files['index.json'];
- if (!indexFile) {
- throw new Error('No dictionary index found in archive');
+ async _getSchema(fileName) {
+ let schemaPromise = this._schemas.get(fileName);
+ if (typeof schemaPromise !== 'undefined') {
+ return schemaPromise;
}
- const index = JSON.parse(await indexFile.async('string'));
- if (!index.title || !index.revision) {
- throw new Error('Unrecognized dictionary format');
+ schemaPromise = requestJson(chrome.runtime.getURL(fileName), 'GET');
+ this._schemas.set(fileName, schemaPromise);
+ return schemaPromise;
+ }
+
+ static _validateJsonSchema(value, schema, fileName) {
+ try {
+ JsonSchema.validate(value, schema);
+ } catch (e) {
+ throw Database._formatSchemaError(e, fileName);
}
+ }
- const summary = {
- title: index.title,
- revision: index.revision,
- sequenced: index.sequenced,
- version: index.format || index.version,
- prefixWildcardsSupported: !!details.prefixWildcardsSupported
- };
+ static _formatSchemaError(e, fileName) {
+ const valuePathString = Database._getSchemaErrorPathString(e.info.valuePath, 'dictionary');
+ const schemaPathString = Database._getSchemaErrorPathString(e.info.schemaPath, 'schema');
- await indexDataLoaded(summary);
+ const e2 = new Error(`Dictionary has invalid data in '${fileName}' for value '${valuePathString}', validated against '${schemaPathString}': ${e.message}`);
+ e2.data = e;
- const buildTermBankName = (index) => `term_bank_${index + 1}.json`;
- const buildTermMetaBankName = (index) => `term_meta_bank_${index + 1}.json`;
- const buildKanjiBankName = (index) => `kanji_bank_${index + 1}.json`;
- const buildKanjiMetaBankName = (index) => `kanji_meta_bank_${index + 1}.json`;
- const buildTagBankName = (index) => `tag_bank_${index + 1}.json`;
+ return e2;
+ }
- const countBanks = (namer) => {
- let count = 0;
- while (zip.files[namer(count)]) {
- ++count;
+ static _getSchemaErrorPathString(infoList, base='') {
+ let result = base;
+ for (const [part] of infoList) {
+ switch (typeof part) {
+ case 'string':
+ if (result.length > 0) {
+ result += '.';
+ }
+ result += part;
+ break;
+ case 'number':
+ result += `[${part}]`;
+ break;
}
+ }
+ return result;
+ }
- return count;
- };
+ static _getDataBankSchemaPaths(version) {
+ const termBank = (
+ version === 1 ?
+ '/bg/data/dictionary-term-bank-v1-schema.json' :
+ '/bg/data/dictionary-term-bank-v3-schema.json'
+ );
+ const termMetaBank = '/bg/data/dictionary-term-meta-bank-v3-schema.json';
+ const kanjiBank = (
+ version === 1 ?
+ '/bg/data/dictionary-kanji-bank-v1-schema.json' :
+ '/bg/data/dictionary-kanji-bank-v3-schema.json'
+ );
+ const kanjiMetaBank = '/bg/data/dictionary-kanji-meta-bank-v3-schema.json';
+ const tagBank = '/bg/data/dictionary-tag-bank-v3-schema.json';
- const termBankCount = countBanks(buildTermBankName);
- const termMetaBankCount = countBanks(buildTermMetaBankName);
- const kanjiBankCount = countBanks(buildKanjiBankName);
- const kanjiMetaBankCount = countBanks(buildKanjiMetaBankName);
- const tagBankCount = countBanks(buildTagBankName);
-
- let bankLoadedCount = 0;
- let bankTotalCount =
- termBankCount +
- termMetaBankCount +
- kanjiBankCount +
- kanjiMetaBankCount +
- tagBankCount;
-
- if (tagDataLoaded && index.tagMeta) {
- const bank = [];
- for (const name in index.tagMeta) {
- const tag = index.tagMeta[name];
- bank.push([name, tag.category, tag.order, tag.notes, tag.score]);
- }
+ return [termBank, termMetaBank, kanjiBank, kanjiMetaBank, tagBank];
+ }
- tagDataLoaded(summary, bank, ++bankTotalCount, bankLoadedCount++);
- }
+ async _dictionaryExists(title) {
+ const db = this.db;
+ const dbCountTransaction = db.transaction(['dictionaries'], 'readonly');
+ const dbIndex = dbCountTransaction.objectStore('dictionaries').index('title');
+ const only = IDBKeyRange.only(title);
+ const count = await Database._getCount(dbIndex, only);
+ return count > 0;
+ }
- const loadBank = async (summary, namer, count, callback) => {
- if (callback) {
- for (let i = 0; i < count; ++i) {
- const bankFile = zip.files[namer(i)];
- const bank = JSON.parse(await bankFile.async('string'));
- await callback(summary, bank, bankTotalCount, bankLoadedCount++);
- }
+ async _findGenericBulk(tableName, indexName, indexValueList, titles, createResult) {
+ this._validate();
+
+ const promises = [];
+ const results = [];
+ const processRow = (row, index) => {
+ if (titles.includes(row.dictionary)) {
+ results.push(createResult(row, index));
}
};
- await loadBank(summary, buildTermBankName, termBankCount, termDataLoaded);
- await loadBank(summary, buildTermMetaBankName, termMetaBankCount, termMetaDataLoaded);
- await loadBank(summary, buildKanjiBankName, kanjiBankCount, kanjiDataLoaded);
- await loadBank(summary, buildKanjiMetaBankName, kanjiMetaBankCount, kanjiMetaDataLoaded);
- await loadBank(summary, buildTagBankName, tagBankCount, tagDataLoaded);
+ const dbTransaction = this.db.transaction([tableName], 'readonly');
+ const dbTerms = dbTransaction.objectStore(tableName);
+ const dbIndex = dbTerms.index(indexName);
+
+ for (let i = 0; i < indexValueList.length; ++i) {
+ const only = IDBKeyRange.only(indexValueList[i]);
+ promises.push(Database._getAll(dbIndex, only, i, processRow));
+ }
+
+ await Promise.all(promises);
- return summary;
+ return results;
}
- static createTerm(row, index) {
+ static _createTerm(row, index) {
return {
index,
expression: row.expression,
@@ -626,7 +613,7 @@ class Database {
};
}
- static createKanji(row, index) {
+ static _createKanji(row, index) {
return {
index,
character: row.character,
@@ -639,20 +626,20 @@ class Database {
};
}
- static createTermMeta({expression, mode, data, dictionary}, index) {
+ static _createTermMeta({expression, mode, data, dictionary}, index) {
return {expression, mode, data, dictionary, index};
}
- static createKanjiMeta({character, mode, data, dictionary}, index) {
+ static _createKanjiMeta({character, mode, data, dictionary}, index) {
return {character, mode, data, dictionary, index};
}
- static getAll(dbIndex, query, context, processRow) {
- const fn = typeof dbIndex.getAll === 'function' ? Database.getAllFast : Database.getAllUsingCursor;
+ static _getAll(dbIndex, query, context, processRow) {
+ const fn = typeof dbIndex.getAll === 'function' ? Database._getAllFast : Database._getAllUsingCursor;
return fn(dbIndex, query, context, processRow);
}
- static getAllFast(dbIndex, query, context, processRow) {
+ static _getAllFast(dbIndex, query, context, processRow) {
return new Promise((resolve, reject) => {
const request = dbIndex.getAll(query);
request.onerror = (e) => reject(e);
@@ -665,7 +652,7 @@ class Database {
});
}
- static getAllUsingCursor(dbIndex, query, context, processRow) {
+ static _getAllUsingCursor(dbIndex, query, context, processRow) {
return new Promise((resolve, reject) => {
const request = dbIndex.openCursor(query, 'next');
request.onerror = (e) => reject(e);
@@ -681,18 +668,18 @@ class Database {
});
}
- static getCounts(targets, query) {
+ static _getCounts(targets, query) {
const countPromises = [];
const counts = {};
for (const [objectStoreName, index] of targets) {
const n = objectStoreName;
- const countPromise = Database.getCount(index, query).then((count) => counts[n] = count);
+ const countPromise = Database._getCount(index, query).then((count) => counts[n] = count);
countPromises.push(countPromise);
}
return Promise.all(countPromises).then(() => counts);
}
- static getCount(dbIndex, query) {
+ static _getCount(dbIndex, query) {
return new Promise((resolve, reject) => {
const request = dbIndex.count(query);
request.onerror = (e) => reject(e);
@@ -700,12 +687,12 @@ class Database {
});
}
- static getAllKeys(dbIndex, query) {
- const fn = typeof dbIndex.getAllKeys === 'function' ? Database.getAllKeysFast : Database.getAllKeysUsingCursor;
+ static _getAllKeys(dbIndex, query) {
+ const fn = typeof dbIndex.getAllKeys === 'function' ? Database._getAllKeysFast : Database._getAllKeysUsingCursor;
return fn(dbIndex, query);
}
- static getAllKeysFast(dbIndex, query) {
+ static _getAllKeysFast(dbIndex, query) {
return new Promise((resolve, reject) => {
const request = dbIndex.getAllKeys(query);
request.onerror = (e) => reject(e);
@@ -713,7 +700,7 @@ class Database {
});
}
- static getAllKeysUsingCursor(dbIndex, query) {
+ static _getAllKeysUsingCursor(dbIndex, query) {
return new Promise((resolve, reject) => {
const primaryKeys = [];
const request = dbIndex.openKeyCursor(query, 'next');
@@ -730,9 +717,9 @@ class Database {
});
}
- static async deleteValues(dbObjectStore, dbIndex, query, onProgress, progressData, progressRate) {
+ static async _deleteValues(dbObjectStore, dbIndex, query, onProgress, progressData, progressRate) {
const hasProgress = (typeof onProgress === 'function');
- const count = await Database.getCount(dbIndex, query);
+ const count = await Database._getCount(dbIndex, query);
++progressData.storesProcesed;
progressData.count += count;
if (hasProgress) {
@@ -751,16 +738,16 @@ class Database {
);
const promises = [];
- const primaryKeys = await Database.getAllKeys(dbIndex, query);
+ const primaryKeys = await Database._getAllKeys(dbIndex, query);
for (const key of primaryKeys) {
- const promise = Database.deleteValue(dbObjectStore, key).then(onValueDeleted);
+ const promise = Database._deleteValue(dbObjectStore, key).then(onValueDeleted);
promises.push(promise);
}
await Promise.all(promises);
}
- static deleteValue(dbObjectStore, key) {
+ static _deleteValue(dbObjectStore, key) {
return new Promise((resolve, reject) => {
const request = dbObjectStore.delete(key);
request.onerror = (e) => reject(e);
@@ -768,7 +755,7 @@ class Database {
});
}
- static bulkAdd(objectStore, items, start, count) {
+ static _bulkAdd(objectStore, items, start, count) {
return new Promise((resolve, reject) => {
if (start + count > items.length) {
count = items.length - start;
@@ -796,7 +783,7 @@ class Database {
});
}
- static open(name, version, onUpgradeNeeded) {
+ static _open(name, version, onUpgradeNeeded) {
return new Promise((resolve, reject) => {
const request = window.indexedDB.open(name, version * 10);
@@ -814,7 +801,7 @@ class Database {
});
}
- static upgrade(db, transaction, oldVersion, upgrades) {
+ static _upgrade(db, transaction, oldVersion, upgrades) {
for (const {version, stores} of upgrades) {
if (oldVersion >= version) { continue; }
@@ -824,13 +811,13 @@ class Database {
const objectStoreNames = transaction.objectStoreNames || db.objectStoreNames;
const objectStore = (
- Database.listContains(objectStoreNames, objectStoreName) ?
+ Database._listContains(objectStoreNames, objectStoreName) ?
transaction.objectStore(objectStoreName) :
db.createObjectStore(objectStoreName, primaryKey)
);
for (const indexName of indices) {
- if (Database.listContains(objectStore.indexNames, indexName)) { continue; }
+ if (Database._listContains(objectStore.indexNames, indexName)) { continue; }
objectStore.createIndex(indexName, indexName, {});
}
@@ -838,7 +825,7 @@ class Database {
}
}
- static deleteDatabase(dbName) {
+ static _deleteDatabase(dbName) {
return new Promise((resolve, reject) => {
const request = indexedDB.deleteDatabase(dbName);
request.onerror = (e) => reject(e);
@@ -846,7 +833,7 @@ class Database {
});
}
- static listContains(list, value) {
+ static _listContains(list, value) {
for (let i = 0, ii = list.length; i < ii; ++i) {
if (list[i] === value) { return true; }
}
diff --git a/test/data/dictionaries/invalid-dictionary1/index.json b/test/data/dictionaries/invalid-dictionary1/index.json
new file mode 100644
index 00000000..1be3b360
--- /dev/null
+++ b/test/data/dictionaries/invalid-dictionary1/index.json
@@ -0,0 +1,7 @@
+{
+ "title": "Invalid Dictionary 1",
+ "format": 0,
+ "revision": "test",
+ "sequenced": true,
+ "description": "Invalid format number"
+} \ No newline at end of file
diff --git a/test/data/dictionaries/invalid-dictionary2/index.json b/test/data/dictionaries/invalid-dictionary2/index.json
new file mode 100644
index 00000000..ba2cc669
--- /dev/null
+++ b/test/data/dictionaries/invalid-dictionary2/index.json
@@ -0,0 +1,7 @@
+{
+ "title": "Invalid Dictionary 2",
+ "format": 3,
+ "revision": "test",
+ "sequenced": true,
+ "description": "Empty entry in kanji bank"
+} \ No newline at end of file
diff --git a/test/data/dictionaries/invalid-dictionary2/kanji_bank_1.json b/test/data/dictionaries/invalid-dictionary2/kanji_bank_1.json
new file mode 100644
index 00000000..5825bcac
--- /dev/null
+++ b/test/data/dictionaries/invalid-dictionary2/kanji_bank_1.json
@@ -0,0 +1,3 @@
+[
+ []
+] \ No newline at end of file
diff --git a/test/data/dictionaries/invalid-dictionary3/index.json b/test/data/dictionaries/invalid-dictionary3/index.json
new file mode 100644
index 00000000..f23fa3f0
--- /dev/null
+++ b/test/data/dictionaries/invalid-dictionary3/index.json
@@ -0,0 +1,7 @@
+{
+ "title": "Invalid Dictionary 3",
+ "format": 3,
+ "revision": "test",
+ "sequenced": true,
+ "description": "Invalid type entry in kanji meta bank"
+} \ No newline at end of file
diff --git a/test/data/dictionaries/invalid-dictionary3/kanji_meta_bank_1.json b/test/data/dictionaries/invalid-dictionary3/kanji_meta_bank_1.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/test/data/dictionaries/invalid-dictionary3/kanji_meta_bank_1.json
@@ -0,0 +1 @@
+{} \ No newline at end of file
diff --git a/test/data/dictionaries/invalid-dictionary4/index.json b/test/data/dictionaries/invalid-dictionary4/index.json
new file mode 100644
index 00000000..542791d7
--- /dev/null
+++ b/test/data/dictionaries/invalid-dictionary4/index.json
@@ -0,0 +1,7 @@
+{
+ "title": "Invalid Dictionary 4",
+ "format": 3,
+ "revision": "test",
+ "sequenced": true,
+ "description": "Invalid value as part of a tag bank entry"
+} \ No newline at end of file
diff --git a/test/data/dictionaries/invalid-dictionary4/tag_bank_1.json b/test/data/dictionaries/invalid-dictionary4/tag_bank_1.json
new file mode 100644
index 00000000..4f19b476
--- /dev/null
+++ b/test/data/dictionaries/invalid-dictionary4/tag_bank_1.json
@@ -0,0 +1,3 @@
+[
+ [{"invalid": true}, "category1", 0, "tag1 notes", 0]
+] \ No newline at end of file
diff --git a/test/data/dictionaries/invalid-dictionary5/index.json b/test/data/dictionaries/invalid-dictionary5/index.json
new file mode 100644
index 00000000..e0d0f00e
--- /dev/null
+++ b/test/data/dictionaries/invalid-dictionary5/index.json
@@ -0,0 +1,7 @@
+{
+ "title": "Invalid Dictionary 5",
+ "format": 3,
+ "revision": "test",
+ "sequenced": true,
+ "description": "Invalid type as part of a term bank entry"
+} \ No newline at end of file
diff --git a/test/data/dictionaries/invalid-dictionary5/term_bank_1.json b/test/data/dictionaries/invalid-dictionary5/term_bank_1.json
new file mode 100644
index 00000000..7288a996
--- /dev/null
+++ b/test/data/dictionaries/invalid-dictionary5/term_bank_1.json
@@ -0,0 +1,3 @@
+[
+ ["打", "だ", "tag1 tag2", "", 2, false, 1, "tag3 tag4 tag5"]
+] \ No newline at end of file
diff --git a/test/data/dictionaries/invalid-dictionary6/index.json b/test/data/dictionaries/invalid-dictionary6/index.json
new file mode 100644
index 00000000..b91acca3
--- /dev/null
+++ b/test/data/dictionaries/invalid-dictionary6/index.json
@@ -0,0 +1,7 @@
+{
+ "title": "Invalid Dictionary 6",
+ "format": 3,
+ "revision": "test",
+ "sequenced": true,
+ "description": "Invalid root type for term meta bank"
+} \ No newline at end of file
diff --git a/test/data/dictionaries/invalid-dictionary6/term_meta_bank_1.json b/test/data/dictionaries/invalid-dictionary6/term_meta_bank_1.json
new file mode 100644
index 00000000..02e4a84d
--- /dev/null
+++ b/test/data/dictionaries/invalid-dictionary6/term_meta_bank_1.json
@@ -0,0 +1 @@
+false \ No newline at end of file
diff --git a/test/test-dictionary-data/index.json b/test/data/dictionaries/valid-dictionary1/index.json
index 3034bf38..3034bf38 100644
--- a/test/test-dictionary-data/index.json
+++ b/test/data/dictionaries/valid-dictionary1/index.json
diff --git a/test/test-dictionary-data/kanji_bank_1.json b/test/data/dictionaries/valid-dictionary1/kanji_bank_1.json
index 264f94c1..264f94c1 100644
--- a/test/test-dictionary-data/kanji_bank_1.json
+++ b/test/data/dictionaries/valid-dictionary1/kanji_bank_1.json
diff --git a/test/test-dictionary-data/kanji_meta_bank_1.json b/test/data/dictionaries/valid-dictionary1/kanji_meta_bank_1.json
index 73e75b8a..73e75b8a 100644
--- a/test/test-dictionary-data/kanji_meta_bank_1.json
+++ b/test/data/dictionaries/valid-dictionary1/kanji_meta_bank_1.json
diff --git a/test/test-dictionary-data/tag_bank_1.json b/test/data/dictionaries/valid-dictionary1/tag_bank_1.json
index 109ad395..109ad395 100644
--- a/test/test-dictionary-data/tag_bank_1.json
+++ b/test/data/dictionaries/valid-dictionary1/tag_bank_1.json
diff --git a/test/test-dictionary-data/tag_bank_2.json b/test/data/dictionaries/valid-dictionary1/tag_bank_2.json
index 5e7936b3..5e7936b3 100644
--- a/test/test-dictionary-data/tag_bank_2.json
+++ b/test/data/dictionaries/valid-dictionary1/tag_bank_2.json
diff --git a/test/test-dictionary-data/term_bank_1.json b/test/data/dictionaries/valid-dictionary1/term_bank_1.json
index 755d9f6a..755d9f6a 100644
--- a/test/test-dictionary-data/term_bank_1.json
+++ b/test/data/dictionaries/valid-dictionary1/term_bank_1.json
diff --git a/test/test-dictionary-data/term_meta_bank_1.json b/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json
index 78096502..78096502 100644
--- a/test/test-dictionary-data/term_meta_bank_1.json
+++ b/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json
diff --git a/test/test-database.js b/test/test-database.js
index fcf519d7..4fb3805d 100644
--- a/test/test-database.js
+++ b/test/test-database.js
@@ -1,3 +1,6 @@
+const fs = require('fs');
+const url = require('url');
+const path = require('path');
const assert = require('assert');
const yomichanTest = require('./yomichan-test');
require('fake-indexeddb/auto');
@@ -5,21 +8,86 @@ require('fake-indexeddb/auto');
const chrome = {
runtime: {
onMessage: {
- addListener: () => { /* NOP */ }
+ addListener() { /* NOP */ }
+ },
+ getURL(path2) {
+ return url.pathToFileURL(path.join(__dirname, '..', 'ext', path2.replace(/^\//, '')));
}
}
};
-const {Database} = yomichanTest.requireScript('ext/bg/js/database.js', ['Database']);
+class XMLHttpRequest {
+ constructor() {
+ this._eventCallbacks = new Map();
+ this._url = '';
+ this._responseText = null;
+ }
+
+ overrideMimeType() {
+ // NOP
+ }
+
+ addEventListener(eventName, callback) {
+ let callbacks = this._eventCallbacks.get(eventName);
+ if (typeof callbacks === 'undefined') {
+ callbacks = [];
+ this._eventCallbacks.set(eventName, callbacks);
+ }
+ callbacks.push(callback);
+ }
+
+ open(action, url) {
+ this._url = url;
+ }
+
+ send() {
+ const filePath = url.fileURLToPath(this._url);
+ Promise.resolve()
+ .then(() => {
+ let source;
+ try {
+ source = fs.readFileSync(filePath, {encoding: 'utf8'});
+ } catch (e) {
+ this._trigger('error');
+ return;
+ }
+ this._responseText = source;
+ this._trigger('load');
+ });
+ }
+
+ get responseText() {
+ return this._responseText;
+ }
+
+ _trigger(eventName, ...args) {
+ const callbacks = this._eventCallbacks.get(eventName);
+ if (typeof callbacks === 'undefined') { return; }
+
+ for (let i = 0, ii = callbacks.length; i < ii; ++i) {
+ callbacks[i](...args);
+ }
+ }
+}
+
+const {JsonSchema} = yomichanTest.requireScript('ext/bg/js/json-schema.js', ['JsonSchema']);
const {dictFieldSplit, dictTagSanitize} = yomichanTest.requireScript('ext/bg/js/dictionary.js', ['dictFieldSplit', 'dictTagSanitize']);
const {stringReverse, hasOwn} = yomichanTest.requireScript('ext/mixed/js/core.js', ['stringReverse', 'hasOwn'], {chrome});
-
-global.window = global;
-global.JSZip = yomichanTest.JSZip;
-global.dictFieldSplit = dictFieldSplit;
-global.dictTagSanitize = dictTagSanitize;
-global.stringReverse = stringReverse;
-global.hasOwn = hasOwn;
+const {requestJson} = yomichanTest.requireScript('ext/bg/js/request.js', ['requestJson'], {XMLHttpRequest});
+
+const databaseGlobals = {
+ chrome,
+ JsonSchema,
+ requestJson,
+ stringReverse,
+ hasOwn,
+ dictFieldSplit,
+ dictTagSanitize,
+ indexedDB: global.indexedDB,
+ JSZip: yomichanTest.JSZip
+};
+databaseGlobals.window = databaseGlobals;
+const {Database} = yomichanTest.requireScript('ext/bg/js/database.js', ['Database'], databaseGlobals);
function countTermsWithExpression(terms, expression) {
@@ -39,21 +107,31 @@ function countKanjiWithCharacter(kanji, character) {
}
-async function clearDatabase() {
- const indexedDB = global.indexedDB;
- for (const {name} of await indexedDB.databases()) {
- await new Promise((resolve, reject) => {
- const request = indexedDB.deleteDatabase(name);
- request.onerror = (e) => reject(e);
- request.onsuccess = () => resolve();
- });
- }
+function clearDatabase(timeout) {
+ return new Promise((resolve, reject) => {
+ const timer = setTimeout(() => {
+ reject(new Error(`clearDatabase failed to resolve after ${timeout}ms`));
+ }, timeout);
+
+ (async () => {
+ const indexedDB = global.indexedDB;
+ for (const {name} of await indexedDB.databases()) {
+ await new Promise((resolve, reject) => {
+ const request = indexedDB.deleteDatabase(name);
+ request.onerror = (e) => reject(e);
+ request.onsuccess = () => resolve();
+ });
+ }
+ clearTimeout(timer);
+ resolve();
+ })();
+ });
}
async function testDatabase1() {
// Load dictionary data
- const testDictionary = yomichanTest.createTestDictionaryArchive();
+ const testDictionary = yomichanTest.createTestDictionaryArchive('valid-dictionary1');
const testDictionarySource = await testDictionary.generateAsync({type: 'string'});
const testDictionaryIndex = JSON.parse(await testDictionary.files['index.json'].async('string'));
@@ -732,7 +810,7 @@ async function testFindTagForTitle1(database, title) {
async function testDatabase2() {
// Load dictionary data
- const testDictionary = yomichanTest.createTestDictionaryArchive();
+ const testDictionary = yomichanTest.createTestDictionaryArchive('valid-dictionary1');
const testDictionarySource = await testDictionary.generateAsync({type: 'string'});
const testDictionaryIndex = JSON.parse(await testDictionary.files['index.json'].async('string'));
@@ -771,12 +849,61 @@ async function testDatabase2() {
}
-async function main() {
- await testDatabase1();
- await clearDatabase();
+async function testDatabase3() {
+ const invalidDictionaries = [
+ 'invalid-dictionary1',
+ 'invalid-dictionary2',
+ 'invalid-dictionary3',
+ 'invalid-dictionary4',
+ 'invalid-dictionary5',
+ 'invalid-dictionary6'
+ ];
- await testDatabase2();
- await clearDatabase();
+ // Setup database
+ const database = new Database();
+ await database.prepare();
+
+ for (const invalidDictionary of invalidDictionaries) {
+ const testDictionary = yomichanTest.createTestDictionaryArchive(invalidDictionary);
+ const testDictionarySource = await testDictionary.generateAsync({type: 'string'});
+
+ let error = null;
+ try {
+ await database.importDictionary(testDictionarySource, () => {}, {});
+ } catch (e) {
+ error = e;
+ }
+
+ if (error === null) {
+ assert.ok(false, `Expected an error while importing ${invalidDictionary}`);
+ } else {
+ const prefix = 'Dictionary has invalid data';
+ const message = error.message;
+ assert.ok(typeof message, 'string');
+ assert.ok(message.startsWith(prefix), `Expected error message to start with '${prefix}': ${message}`);
+ }
+ }
+
+ await database.close();
+}
+
+
+async function main() {
+ const clearTimeout = 5000;
+ try {
+ await testDatabase1();
+ await clearDatabase(clearTimeout);
+
+ await testDatabase2();
+ await clearDatabase(clearTimeout);
+
+ await testDatabase3();
+ await clearDatabase(clearTimeout);
+ } catch (e) {
+ console.log(e);
+ process.exit(-1);
+ throw e;
+ }
}
diff --git a/test/test-dictionary.js b/test/test-dictionary.js
index 84014540..b157dd5d 100644
--- a/test/test-dictionary.js
+++ b/test/test-dictionary.js
@@ -3,9 +3,38 @@ const dictionaryValidate = require('./dictionary-validate');
async function main() {
- const archive = yomichanTest.createTestDictionaryArchive();
+ const dictionaries = [
+ {name: 'valid-dictionary1', valid: true},
+ {name: 'invalid-dictionary1', valid: false},
+ {name: 'invalid-dictionary2', valid: false},
+ {name: 'invalid-dictionary3', valid: false},
+ {name: 'invalid-dictionary4', valid: false},
+ {name: 'invalid-dictionary5', valid: false},
+ {name: 'invalid-dictionary6', valid: false}
+ ];
+
const schemas = dictionaryValidate.getSchemas();
- await dictionaryValidate.validateDictionary(archive, schemas);
+
+ for (const {name, valid} of dictionaries) {
+ const archive = yomichanTest.createTestDictionaryArchive(name);
+
+ let error = null;
+ try {
+ await dictionaryValidate.validateDictionary(archive, schemas);
+ } catch (e) {
+ error = e;
+ }
+
+ if (valid) {
+ if (error !== null) {
+ throw error;
+ }
+ } else {
+ if (error === null) {
+ throw new Error(`Expected dictionary ${name} to be invalid`);
+ }
+ }
+ }
}
diff --git a/test/yomichan-test.js b/test/yomichan-test.js
index dd4da919..939e0ad2 100644
--- a/test/yomichan-test.js
+++ b/test/yomichan-test.js
@@ -26,21 +26,14 @@ function getJSZip() {
return JSZip;
}
-function createTestDictionaryArchive(dictionaryName) {
- const fileNames = [
- 'index.json',
- 'tag_bank_1.json',
- 'tag_bank_2.json',
- 'term_bank_1.json',
- 'kanji_bank_1.json',
- 'term_meta_bank_1.json',
- 'kanji_meta_bank_1.json'
- ];
+function createTestDictionaryArchive(dictionary, dictionaryName) {
+ const dictionaryDirectory = path.join(__dirname, 'data', 'dictionaries', dictionary);
+ const fileNames = fs.readdirSync(dictionaryDirectory);
const archive = new (getJSZip())();
for (const fileName of fileNames) {
- const source = fs.readFileSync(path.join(__dirname, 'test-dictionary-data', fileName), {encoding: 'utf8'});
+ const source = fs.readFileSync(path.join(dictionaryDirectory, fileName), {encoding: 'utf8'});
const json = JSON.parse(source);
if (fileName === 'index.json' && typeof dictionaryName === 'string') {
json.title = dictionaryName;