aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorAlex Yatskov <alex@foosoft.net>2020-02-24 21:31:14 -0800
committerAlex Yatskov <alex@foosoft.net>2020-02-24 21:31:14 -0800
commitd32f4def0eeed1599857bc04c973337a2a13dd8b (patch)
tree61149656f361dd2d9998d67d68249dc184b73fbb /test
parent0c5b9b1fa1599cbf769d96cdebc226310f9dd8bc (diff)
parent706c3edcffb0078d71fd5b58775f16cf5fc1205b (diff)
Merge branch 'master' into testing
Diffstat (limited to 'test')
-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.json6
-rw-r--r--test/data/dictionaries/valid-dictionary1/kanji_bank_1.json42
-rw-r--r--test/data/dictionaries/valid-dictionary1/kanji_meta_bank_1.json4
-rw-r--r--test/data/dictionaries/valid-dictionary1/tag_bank_1.json7
-rw-r--r--test/data/dictionaries/valid-dictionary1/tag_bank_2.json9
-rw-r--r--test/data/dictionaries/valid-dictionary1/term_bank_1.json34
-rw-r--r--test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json5
-rw-r--r--test/dictionary-validate.js113
-rw-r--r--test/schema-validate.js52
-rw-r--r--test/test-database.js935
-rw-r--r--test/test-dictionary.js59
-rw-r--r--test/test-schema.js251
-rw-r--r--test/yomichan-test.js70
24 files changed, 1640 insertions, 0 deletions
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/data/dictionaries/valid-dictionary1/index.json b/test/data/dictionaries/valid-dictionary1/index.json
new file mode 100644
index 00000000..3034bf38
--- /dev/null
+++ b/test/data/dictionaries/valid-dictionary1/index.json
@@ -0,0 +1,6 @@
+{
+ "title": "Test Dictionary",
+ "format": 3,
+ "revision": "test",
+ "sequenced": true
+} \ No newline at end of file
diff --git a/test/data/dictionaries/valid-dictionary1/kanji_bank_1.json b/test/data/dictionaries/valid-dictionary1/kanji_bank_1.json
new file mode 100644
index 00000000..264f94c1
--- /dev/null
+++ b/test/data/dictionaries/valid-dictionary1/kanji_bank_1.json
@@ -0,0 +1,42 @@
+[
+ [
+ "打",
+ "ダ ダアス",
+ "う.つ う.ち- ぶ.つ",
+ "ktag1 ktag2",
+ [
+ "meaning1",
+ "meaning2",
+ "meaning3",
+ "meaning4",
+ "meaning5"
+ ],
+ {
+ "kstat1": "1",
+ "kstat2": "2",
+ "kstat3": "3",
+ "kstat4": "4",
+ "kstat5": "5"
+ }
+ ],
+ [
+ "込",
+ "",
+ "-こ.む こ.む こ.み -こ.み こ.める",
+ "ktag1 ktag2",
+ [
+ "meaning1",
+ "meaning2",
+ "meaning3",
+ "meaning4",
+ "meaning5"
+ ],
+ {
+ "kstat1": "1",
+ "kstat2": "2",
+ "kstat3": "3",
+ "kstat4": "4",
+ "kstat5": "5"
+ }
+ ]
+] \ No newline at end of file
diff --git a/test/data/dictionaries/valid-dictionary1/kanji_meta_bank_1.json b/test/data/dictionaries/valid-dictionary1/kanji_meta_bank_1.json
new file mode 100644
index 00000000..73e75b8a
--- /dev/null
+++ b/test/data/dictionaries/valid-dictionary1/kanji_meta_bank_1.json
@@ -0,0 +1,4 @@
+[
+ ["打", "freq", 1],
+ ["込", "freq", 2]
+] \ No newline at end of file
diff --git a/test/data/dictionaries/valid-dictionary1/tag_bank_1.json b/test/data/dictionaries/valid-dictionary1/tag_bank_1.json
new file mode 100644
index 00000000..109ad395
--- /dev/null
+++ b/test/data/dictionaries/valid-dictionary1/tag_bank_1.json
@@ -0,0 +1,7 @@
+[
+ ["tag1", "category1", 0, "tag1 notes", 0],
+ ["tag2", "category2", 0, "tag2 notes", 0],
+ ["tag3", "category3", 0, "tag3 notes", 0],
+ ["tag4", "category4", 0, "tag4 notes", 0],
+ ["tag5", "category5", 0, "tag5 notes", 0]
+] \ No newline at end of file
diff --git a/test/data/dictionaries/valid-dictionary1/tag_bank_2.json b/test/data/dictionaries/valid-dictionary1/tag_bank_2.json
new file mode 100644
index 00000000..5e7936b3
--- /dev/null
+++ b/test/data/dictionaries/valid-dictionary1/tag_bank_2.json
@@ -0,0 +1,9 @@
+[
+ ["ktag1", "kcategory1", 0, "ktag1 notes", 0],
+ ["ktag2", "kcategory2", 0, "ktag2 notes", 0],
+ ["kstat1", "kcategory3", 0, "kstat1 notes", 0],
+ ["kstat2", "kcategory4", 0, "kstat2 notes", 0],
+ ["kstat3", "kcategory5", 0, "kstat3 notes", 0],
+ ["kstat4", "kcategory6", 0, "kstat4 notes", 0],
+ ["kstat5", "kcategory7", 0, "kstat5 notes", 0]
+] \ No newline at end of file
diff --git a/test/data/dictionaries/valid-dictionary1/term_bank_1.json b/test/data/dictionaries/valid-dictionary1/term_bank_1.json
new file mode 100644
index 00000000..755d9f6a
--- /dev/null
+++ b/test/data/dictionaries/valid-dictionary1/term_bank_1.json
@@ -0,0 +1,34 @@
+[
+ ["打", "だ", "tag1 tag2", "", 2, ["definition1a (打, だ)", "definition1b (打, だ)"], 1, "tag3 tag4 tag5"],
+ ["打", "ダース", "tag1 tag2", "", 1, ["definition1a (打, ダース)", "definition1b (打, ダース)"], 2, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 3, ["definition1a (打つ, うつ)", "definition1b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 4, ["definition2a (打つ, うつ)", "definition2b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 5, ["definition3a (打つ, うつ)", "definition3b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 6, ["definition4a (打つ, うつ)", "definition4b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 7, ["definition5a (打つ, うつ)", "definition5b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 8, ["definition6a (打つ, うつ)", "definition6b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 9, ["definition7a (打つ, うつ)", "definition7b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 10, ["definition8a (打つ, うつ)", "definition8b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 11, ["definition9a (打つ, うつ)", "definition9b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 12, ["definition10a (打つ, うつ)", "definition10b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 13, ["definition11a (打つ, うつ)", "definition11b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 14, ["definition12a (打つ, うつ)", "definition12b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 15, ["definition13a (打つ, うつ)", "definition13b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 16, ["definition14a (打つ, うつ)", "definition14b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "うつ", "tag1 tag2", "v5", 17, ["definition15a (打つ, うつ)", "definition15b (打つ, うつ)"], 3, "tag3 tag4 tag5"],
+ ["打つ", "ぶつ", "tag1 tag2", "v5", 18, ["definition1a (打つ, ぶつ)", "definition1b (打つ, ぶつ)"], 4, "tag3 tag4 tag5"],
+ ["打つ", "ぶつ", "tag1 tag2", "v5", 19, ["definition2a (打つ, ぶつ)", "definition2b (打つ, ぶつ)"], 4, "tag3 tag4 tag5"],
+ ["打ち込む", "うちこむ", "tag1 tag2", "v5", 20, ["definition1a (打ち込む, うちこむ)", "definition1b (打ち込む, うちこむ)"], 5, "tag3 tag4 tag5"],
+ ["打ち込む", "うちこむ", "tag1 tag2", "v5", 21, ["definition2a (打ち込む, うちこむ)", "definition2b (打ち込む, うちこむ)"], 5, "tag5 tag6 tag7"],
+ ["打ち込む", "うちこむ", "tag1 tag2", "v5", 22, ["definition3a (打ち込む, うちこむ)", "definition3b (打ち込む, うちこむ)"], 5, "tag5 tag6 tag7"],
+ ["打ち込む", "うちこむ", "tag1 tag2", "v5", 23, ["definition4a (打ち込む, うちこむ)", "definition4b (打ち込む, うちこむ)"], 5, "tag5 tag6 tag7"],
+ ["打ち込む", "うちこむ", "tag1 tag2", "v5", 24, ["definition5a (打ち込む, うちこむ)", "definition5b (打ち込む, うちこむ)"], 5, "tag5 tag6 tag7"],
+ ["打ち込む", "うちこむ", "tag1 tag2", "v5", 25, ["definition6a (打ち込む, うちこむ)", "definition6b (打ち込む, うちこむ)"], 5, "tag5 tag6 tag7"],
+ ["打ち込む", "うちこむ", "tag1 tag2", "v5", 26, ["definition7a (打ち込む, うちこむ)", "definition7b (打ち込む, うちこむ)"], 5, "tag5 tag6 tag7"],
+ ["打ち込む", "うちこむ", "tag1 tag2", "v5", 27, ["definition8a (打ち込む, うちこむ)", "definition8b (打ち込む, うちこむ)"], 5, "tag5 tag6 tag7"],
+ ["打ち込む", "うちこむ", "tag1 tag2", "v5", 28, ["definition9a (打ち込む, うちこむ)", "definition9b (打ち込む, うちこむ)"], 5, "tag5 tag6 tag7"],
+ ["打ち込む", "ぶちこむ", "tag1 tag2", "v5", 29, ["definition1a (打ち込む, ぶちこむ)", "definition1b (打ち込む, ぶちこむ)"], 6, "tag3 tag4 tag5"],
+ ["打ち込む", "ぶちこむ", "tag1 tag2", "v5", 30, ["definition2a (打ち込む, ぶちこむ)", "definition2b (打ち込む, ぶちこむ)"], 6, "tag3 tag4 tag5"],
+ ["打ち込む", "ぶちこむ", "tag1 tag2", "v5", 31, ["definition3a (打ち込む, ぶちこむ)", "definition3b (打ち込む, ぶちこむ)"], 6, "tag3 tag4 tag5"],
+ ["打ち込む", "ぶちこむ", "tag1 tag2", "v5", 32, ["definition4a (打ち込む, ぶちこむ)", "definition4b (打ち込む, ぶちこむ)"], 6, "tag3 tag4 tag5"]
+] \ No newline at end of file
diff --git a/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json b/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json
new file mode 100644
index 00000000..78096502
--- /dev/null
+++ b/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json
@@ -0,0 +1,5 @@
+[
+ ["打", "freq", 1],
+ ["打つ", "freq", 2],
+ ["打ち込む", "freq", 3]
+] \ No newline at end of file
diff --git a/test/dictionary-validate.js b/test/dictionary-validate.js
new file mode 100644
index 00000000..14eee2ed
--- /dev/null
+++ b/test/dictionary-validate.js
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2020 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * 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/>.
+ */
+
+const fs = require('fs');
+const path = require('path');
+const yomichanTest = require('./yomichan-test');
+
+const JSZip = yomichanTest.JSZip;
+const {JsonSchema} = yomichanTest.requireScript('ext/bg/js/json-schema.js', ['JsonSchema']);
+
+
+function readSchema(relativeFileName) {
+ const fileName = path.join(__dirname, relativeFileName);
+ const source = fs.readFileSync(fileName, {encoding: 'utf8'});
+ return JSON.parse(source);
+}
+
+
+async function validateDictionaryBanks(zip, fileNameFormat, schema) {
+ let index = 1;
+ while (true) {
+ const fileName = fileNameFormat.replace(/\?/, index);
+
+ const file = zip.files[fileName];
+ if (!file) { break; }
+
+ const data = JSON.parse(await file.async('string'));
+ JsonSchema.validate(data, schema);
+
+ ++index;
+ }
+}
+
+async function validateDictionary(archive, schemas) {
+ const indexFile = archive.files['index.json'];
+ if (!indexFile) {
+ throw new Error('No dictionary index found in archive');
+ }
+
+ const index = JSON.parse(await indexFile.async('string'));
+ const version = index.format || index.version;
+
+ JsonSchema.validate(index, schemas.index);
+
+ await validateDictionaryBanks(archive, 'term_bank_?.json', version === 1 ? schemas.termBankV1 : schemas.termBankV3);
+ await validateDictionaryBanks(archive, 'term_meta_bank_?.json', schemas.termMetaBankV3);
+ await validateDictionaryBanks(archive, 'kanji_bank_?.json', version === 1 ? schemas.kanjiBankV1 : schemas.kanjiBankV3);
+ await validateDictionaryBanks(archive, 'kanji_meta_bank_?.json', schemas.kanjiMetaBankV3);
+ await validateDictionaryBanks(archive, 'tag_bank_?.json', schemas.tagBankV3);
+}
+
+function getSchemas() {
+ return {
+ index: readSchema('../ext/bg/data/dictionary-index-schema.json'),
+ kanjiBankV1: readSchema('../ext/bg/data/dictionary-kanji-bank-v1-schema.json'),
+ kanjiBankV3: readSchema('../ext/bg/data/dictionary-kanji-bank-v3-schema.json'),
+ kanjiMetaBankV3: readSchema('../ext/bg/data/dictionary-kanji-meta-bank-v3-schema.json'),
+ tagBankV3: readSchema('../ext/bg/data/dictionary-tag-bank-v3-schema.json'),
+ termBankV1: readSchema('../ext/bg/data/dictionary-term-bank-v1-schema.json'),
+ termBankV3: readSchema('../ext/bg/data/dictionary-term-bank-v3-schema.json'),
+ termMetaBankV3: readSchema('../ext/bg/data/dictionary-term-meta-bank-v3-schema.json')
+ };
+}
+
+
+async function main() {
+ const dictionaryFileNames = process.argv.slice(2);
+ if (dictionaryFileNames.length === 0) {
+ console.log([
+ 'Usage:',
+ ' node dictionary-validate <dictionary-file-names>...'
+ ].join('\n'));
+ return;
+ }
+
+ const schemas = getSchemas();
+
+ for (const dictionaryFileName of dictionaryFileNames) {
+ try {
+ console.log(`Validating ${dictionaryFileName}...`);
+ const source = fs.readFileSync(dictionaryFileName);
+ const archive = await JSZip.loadAsync(source);
+ await validateDictionary(archive, schemas);
+ console.log('No issues found');
+ } catch (e) {
+ console.warn(e);
+ }
+ }
+}
+
+
+if (require.main === module) { main(); }
+
+
+module.exports = {
+ getSchemas,
+ validateDictionary
+};
diff --git a/test/schema-validate.js b/test/schema-validate.js
new file mode 100644
index 00000000..a4f2d94c
--- /dev/null
+++ b/test/schema-validate.js
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2020 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * 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/>.
+ */
+
+const fs = require('fs');
+const yomichanTest = require('./yomichan-test');
+
+const {JsonSchema} = yomichanTest.requireScript('ext/bg/js/json-schema.js', ['JsonSchema']);
+
+
+function main() {
+ const args = process.argv.slice(2);
+ if (args.length < 2) {
+ console.log([
+ 'Usage:',
+ ' node schema-validate <schema-file-name> <data-file-names>...'
+ ].join('\n'));
+ return;
+ }
+
+ const schemaSource = fs.readFileSync(args[0], {encoding: 'utf8'});
+ const schema = JSON.parse(schemaSource);
+
+ for (const dataFileName of args.slice(1)) {
+ try {
+ console.log(`Validating ${dataFileName}...`);
+ const dataSource = fs.readFileSync(dataFileName, {encoding: 'utf8'});
+ const data = JSON.parse(dataSource);
+ JsonSchema.validate(data, schema);
+ console.log('No issues found');
+ } catch (e) {
+ console.warn(e);
+ }
+ }
+}
+
+
+if (require.main === module) { main(); }
diff --git a/test/test-database.js b/test/test-database.js
new file mode 100644
index 00000000..c2317881
--- /dev/null
+++ b/test/test-database.js
@@ -0,0 +1,935 @@
+/*
+ * Copyright (C) 2020 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * 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/>.
+ */
+
+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');
+
+const chrome = {
+ runtime: {
+ onMessage: {
+ addListener() { /* NOP */ }
+ },
+ getURL(path2) {
+ return url.pathToFileURL(path.join(__dirname, '..', 'ext', path2.replace(/^\//, '')));
+ }
+ }
+};
+
+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, url2) {
+ this._url = url2;
+ }
+
+ 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});
+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) {
+ return terms.reduce((i, v) => (i + (v.expression === expression ? 1 : 0)), 0);
+}
+
+function countTermsWithReading(terms, reading) {
+ return terms.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);
+}
+
+
+function clearDatabase(timeout) {
+ return new Promise((resolve, reject) => {
+ let timer = setTimeout(() => {
+ timer = null;
+ 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((resolve2, reject2) => {
+ const request = indexedDB.deleteDatabase(name);
+ request.onerror = (e) => reject2(e);
+ request.onsuccess = () => resolve2();
+ });
+ }
+ if (timer !== null) {
+ clearTimeout(timer);
+ }
+ resolve();
+ })();
+ });
+}
+
+
+async function testDatabase1() {
+ // Load dictionary data
+ const testDictionary = yomichanTest.createTestDictionaryArchive('valid-dictionary1');
+ const testDictionarySource = await testDictionary.generateAsync({type: 'string'});
+ 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 database.purge();
+ await testDatabaseEmpty1(database);
+ }
+ },
+ {
+ cleanup: async () => {
+ // Test deleteDictionary
+ let progressEvent = false;
+ await database.deleteDictionary(
+ title,
+ () => {
+ progressEvent = true;
+ },
+ {rate: 1000}
+ );
+ assert.ok(progressEvent);
+
+ await testDatabaseEmpty1(database);
+ }
+ },
+ {
+ cleanup: async () => {}
+ }
+ ];
+
+ // Setup database
+ const database = new Database();
+ await database.prepare();
+
+ for (const {cleanup} of iterations) {
+ const expectedSummary = {
+ title,
+ revision: 'test',
+ sequenced: true,
+ version: 3,
+ prefixWildcardsSupported: true
+ };
+
+ // Import data
+ let progressEvent = false;
+ const {result, errors} = await database.importDictionary(
+ testDictionarySource,
+ () => {
+ progressEvent = true;
+ },
+ {prefixWildcardsSupported: true}
+ );
+ assert.deepStrictEqual(errors, []);
+ assert.deepStrictEqual(result, expectedSummary);
+ assert.ok(progressEvent);
+
+ // Get info summary
+ const info = await database.getDictionaryInfo();
+ assert.deepStrictEqual(info, [expectedSummary]);
+
+ // Get counts
+ const counts = await database.getDictionaryCounts(
+ info.map((v) => v.title),
+ true
+ );
+ assert.deepStrictEqual(counts, {
+ counts: [{kanji: 2, kanjiMeta: 2, terms: 32, termMeta: 3, tagMeta: 12}],
+ total: {kanji: 2, kanjiMeta: 2, terms: 32, termMeta: 3, tagMeta: 12}
+ });
+
+ // Test find* functions
+ await testFindTermsBulkTest1(database, titles);
+ await testTindTermsExactBulk1(database, titles);
+ await testFindTermsBySequenceBulk1(database, title);
+ await testFindTermMetaBulk1(database, titles);
+ await testFindKanjiBulk1(database, titles);
+ await testFindKanjiMetaBulk1(database, titles);
+ await testFindTagForTitle1(database, title);
+
+ // Cleanup
+ await cleanup();
+ }
+
+ await database.close();
+}
+
+async function testDatabaseEmpty1(database) {
+ const info = await database.getDictionaryInfo();
+ assert.deepStrictEqual(info, []);
+
+ const counts = await database.getDictionaryCounts([], true);
+ assert.deepStrictEqual(counts, {
+ counts: [],
+ total: {kanji: 0, kanjiMeta: 0, terms: 0, termMeta: 0, tagMeta: 0}
+ });
+}
+
+async function testFindTermsBulkTest1(database, titles) {
+ const data = [
+ {
+ inputs: [
+ {
+ wildcard: null,
+ termList: ['打', '打つ', '打ち込む']
+ },
+ {
+ wildcard: null,
+ termList: ['だ', 'ダース', 'うつ', 'ぶつ', 'うちこむ', 'ぶちこむ']
+ },
+ {
+ wildcard: 'suffix',
+ termList: ['打']
+ }
+ ],
+ expectedResults: {
+ total: 32,
+ expressions: [
+ ['打', 2],
+ ['打つ', 17],
+ ['打ち込む', 13]
+ ],
+ readings: [
+ ['だ', 1],
+ ['ダース', 1],
+ ['うつ', 15],
+ ['ぶつ', 2],
+ ['うちこむ', 9],
+ ['ぶちこむ', 4]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ wildcard: null,
+ termList: ['込む']
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ expressions: [],
+ readings: []
+ }
+ },
+ {
+ inputs: [
+ {
+ wildcard: 'prefix',
+ termList: ['込む']
+ }
+ ],
+ expectedResults: {
+ total: 13,
+ expressions: [
+ ['打ち込む', 13]
+ ],
+ readings: [
+ ['うちこむ', 9],
+ ['ぶちこむ', 4]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ wildcard: null,
+ termList: []
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ expressions: [],
+ readings: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {termList, wildcard} of inputs) {
+ const results = await database.findTermsBulk(termList, titles, wildcard);
+ assert.strictEqual(results.length, expectedResults.total);
+ for (const [expression, count] of expectedResults.expressions) {
+ assert.strictEqual(countTermsWithExpression(results, expression), count);
+ }
+ for (const [reading, count] of expectedResults.readings) {
+ assert.strictEqual(countTermsWithReading(results, reading), count);
+ }
+ }
+ }
+}
+
+async function testTindTermsExactBulk1(database, titles) {
+ const data = [
+ {
+ inputs: [
+ {
+ termList: ['打', '打つ', '打ち込む'],
+ readingList: ['だ', 'うつ', 'うちこむ']
+ }
+ ],
+ expectedResults: {
+ total: 25,
+ expressions: [
+ ['打', 1],
+ ['打つ', 15],
+ ['打ち込む', 9]
+ ],
+ readings: [
+ ['だ', 1],
+ ['うつ', 15],
+ ['うちこむ', 9]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: ['打', '打つ', '打ち込む'],
+ readingList: ['だ?', 'うつ?', 'うちこむ?']
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ expressions: [],
+ readings: []
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: ['打つ', '打つ'],
+ readingList: ['うつ', 'ぶつ']
+ }
+ ],
+ expectedResults: {
+ total: 17,
+ expressions: [
+ ['打つ', 17]
+ ],
+ readings: [
+ ['うつ', 15],
+ ['ぶつ', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: ['打つ'],
+ readingList: ['うちこむ']
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ expressions: [],
+ readings: []
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: [],
+ readingList: []
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ expressions: [],
+ readings: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {termList, readingList} of inputs) {
+ const results = await database.findTermsExactBulk(termList, readingList, titles);
+ assert.strictEqual(results.length, expectedResults.total);
+ for (const [expression, count] of expectedResults.expressions) {
+ assert.strictEqual(countTermsWithExpression(results, expression), count);
+ }
+ for (const [reading, count] of expectedResults.readings) {
+ assert.strictEqual(countTermsWithReading(results, reading), count);
+ }
+ }
+ }
+}
+
+async function testFindTermsBySequenceBulk1(database, mainDictionary) {
+ const data = [
+ {
+ inputs: [
+ {
+ sequenceList: [1, 2, 3, 4, 5, 6]
+ }
+ ],
+ expectedResults: {
+ total: 32,
+ expressions: [
+ ['打', 2],
+ ['打つ', 17],
+ ['打ち込む', 13]
+ ],
+ readings: [
+ ['だ', 1],
+ ['ダース', 1],
+ ['うつ', 15],
+ ['ぶつ', 2],
+ ['うちこむ', 9],
+ ['ぶちこむ', 4]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [1]
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ expressions: [
+ ['打', 1]
+ ],
+ readings: [
+ ['だ', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [2]
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ expressions: [
+ ['打', 1]
+ ],
+ readings: [
+ ['ダース', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [3]
+ }
+ ],
+ expectedResults: {
+ total: 15,
+ expressions: [
+ ['打つ', 15]
+ ],
+ readings: [
+ ['うつ', 15]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [4]
+ }
+ ],
+ expectedResults: {
+ total: 2,
+ expressions: [
+ ['打つ', 2]
+ ],
+ readings: [
+ ['ぶつ', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [5]
+ }
+ ],
+ expectedResults: {
+ total: 9,
+ expressions: [
+ ['打ち込む', 9]
+ ],
+ readings: [
+ ['うちこむ', 9]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [6]
+ }
+ ],
+ expectedResults: {
+ total: 4,
+ expressions: [
+ ['打ち込む', 4]
+ ],
+ readings: [
+ ['ぶちこむ', 4]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [-1]
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ expressions: [],
+ readings: []
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: []
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ expressions: [],
+ readings: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {sequenceList} of inputs) {
+ const results = await database.findTermsBySequenceBulk(sequenceList, mainDictionary);
+ assert.strictEqual(results.length, expectedResults.total);
+ for (const [expression, count] of expectedResults.expressions) {
+ assert.strictEqual(countTermsWithExpression(results, expression), count);
+ }
+ for (const [reading, count] of expectedResults.readings) {
+ assert.strictEqual(countTermsWithReading(results, reading), count);
+ }
+ }
+ }
+}
+
+async function testFindTermMetaBulk1(database, titles) {
+ const data = [
+ {
+ inputs: [
+ {
+ termList: ['打']
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ modes: [
+ ['freq', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: ['打つ']
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ modes: [
+ ['freq', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: ['打ち込む']
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ modes: [
+ ['freq', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: ['?']
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ modes: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {termList} of inputs) {
+ const results = await database.findTermMetaBulk(termList, titles);
+ assert.strictEqual(results.length, expectedResults.total);
+ for (const [mode, count] of expectedResults.modes) {
+ assert.strictEqual(countMetasWithMode(results, mode), count);
+ }
+ }
+ }
+}
+
+async function testFindKanjiBulk1(database, titles) {
+ 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);
+ assert.strictEqual(results.length, expectedResults.total);
+ for (const [kanji, count] of expectedResults.kanji) {
+ assert.strictEqual(countKanjiWithCharacter(results, kanji), count);
+ }
+ }
+ }
+}
+
+async function testFindKanjiMetaBulk1(database, titles) {
+ const data = [
+ {
+ inputs: [
+ {
+ kanjiList: ['打']
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ modes: [
+ ['freq', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ kanjiList: ['込']
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ modes: [
+ ['freq', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ kanjiList: ['?']
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ modes: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {kanjiList} of inputs) {
+ const results = await database.findKanjiMetaBulk(kanjiList, titles);
+ assert.strictEqual(results.length, expectedResults.total);
+ for (const [mode, count] of expectedResults.modes) {
+ assert.strictEqual(countMetasWithMode(results, mode), count);
+ }
+ }
+ }
+}
+
+async function testFindTagForTitle1(database, title) {
+ const data = [
+ {
+ inputs: [
+ {
+ name: 'tag1'
+ }
+ ],
+ expectedResults: {
+ value: {category: 'category1', dictionary: title, name: 'tag1', notes: 'tag1 notes', order: 0, score: 0}
+ }
+ },
+ {
+ inputs: [
+ {
+ name: 'ktag1'
+ }
+ ],
+ expectedResults: {
+ value: {category: 'kcategory1', dictionary: title, name: 'ktag1', notes: 'ktag1 notes', order: 0, score: 0}
+ }
+ },
+ {
+ inputs: [
+ {
+ name: 'kstat1'
+ }
+ ],
+ expectedResults: {
+ value: {category: 'kcategory3', dictionary: title, name: 'kstat1', notes: 'kstat1 notes', 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);
+ assert.deepStrictEqual(result, expectedResults.value);
+ }
+ }
+}
+
+
+async function testDatabase2() {
+ // Load dictionary data
+ const testDictionary = yomichanTest.createTestDictionaryArchive('valid-dictionary1');
+ const testDictionarySource = await testDictionary.generateAsync({type: 'string'});
+ 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 database = new Database();
+
+ // Error: not prepared
+ await assert.rejects(async () => await database.purge());
+ await assert.rejects(async () => await database.deleteDictionary(title, () => {}, {}));
+ await assert.rejects(async () => await database.findTermsBulk(['?'], titles, null));
+ await assert.rejects(async () => await database.findTermsExactBulk(['?'], ['?'], titles));
+ await assert.rejects(async () => await database.findTermsBySequenceBulk([1], title));
+ await assert.rejects(async () => await database.findTermMetaBulk(['?'], titles));
+ await assert.rejects(async () => await database.findTermMetaBulk(['?'], titles));
+ await assert.rejects(async () => await database.findKanjiBulk(['?'], titles));
+ await assert.rejects(async () => await database.findKanjiMetaBulk(['?'], titles));
+ await assert.rejects(async () => await database.findTagForTitle('tag', title));
+ await assert.rejects(async () => await database.getDictionaryInfo());
+ await assert.rejects(async () => await database.getDictionaryCounts(titles, true));
+ await assert.rejects(async () => await database.importDictionary(testDictionarySource, () => {}, {}));
+
+ await database.prepare();
+
+ // Error: already prepared
+ await assert.rejects(async () => await database.prepare());
+
+ await database.importDictionary(testDictionarySource, () => {}, {});
+
+ // Error: dictionary already imported
+ await assert.rejects(async () => await database.importDictionary(testDictionarySource, () => {}, {}));
+
+ await database.close();
+}
+
+
+async function testDatabase3() {
+ const invalidDictionaries = [
+ 'invalid-dictionary1',
+ 'invalid-dictionary2',
+ 'invalid-dictionary3',
+ 'invalid-dictionary4',
+ 'invalid-dictionary5',
+ 'invalid-dictionary6'
+ ];
+
+ // 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;
+ }
+}
+
+
+if (require.main === module) { main(); }
diff --git a/test/test-dictionary.js b/test/test-dictionary.js
new file mode 100644
index 00000000..74f9e62b
--- /dev/null
+++ b/test/test-dictionary.js
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2020 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * 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/>.
+ */
+
+const yomichanTest = require('./yomichan-test');
+const dictionaryValidate = require('./dictionary-validate');
+
+
+async function main() {
+ 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();
+
+ 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`);
+ }
+ }
+ }
+}
+
+
+if (require.main === module) { main(); }
diff --git a/test/test-schema.js b/test/test-schema.js
new file mode 100644
index 00000000..f4612f86
--- /dev/null
+++ b/test/test-schema.js
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2020 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * 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/>.
+ */
+
+const assert = require('assert');
+const yomichanTest = require('./yomichan-test');
+
+const {JsonSchema} = yomichanTest.requireScript('ext/bg/js/json-schema.js', ['JsonSchema']);
+
+
+function testValidate1() {
+ const schema = {
+ allOf: [
+ {
+ type: 'number'
+ },
+ {
+ anyOf: [
+ {minimum: 10, maximum: 100},
+ {minimum: -100, maximum: -10}
+ ]
+ },
+ {
+ oneOf: [
+ {multipleOf: 3},
+ {multipleOf: 5}
+ ]
+ },
+ {
+ not: [
+ {multipleOf: 20}
+ ]
+ }
+ ]
+ };
+
+ const schemaValidate = (value) => {
+ try {
+ JsonSchema.validate(value, schema);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ };
+
+ const jsValidate = (value) => {
+ return (
+ typeof value === 'number' &&
+ (
+ (value >= 10 && value <= 100) ||
+ (value >= -100 && value <= -10)
+ ) &&
+ (
+ (
+ (value % 3) === 0 ||
+ (value % 5) === 0
+ ) &&
+ (value % 15) !== 0
+ ) &&
+ (value % 20) !== 0
+ );
+ };
+
+ for (let i = -111; i <= 111; i++) {
+ const actual = schemaValidate(i, schema);
+ const expected = jsValidate(i);
+ assert.strictEqual(actual, expected);
+ }
+}
+
+
+function testGetValidValueOrDefault1() {
+ // Test value defaulting on objects with additionalProperties=false
+ const schema = {
+ type: 'object',
+ required: ['test'],
+ properties: {
+ test: {
+ type: 'string',
+ default: 'default'
+ }
+ },
+ additionalProperties: false
+ };
+
+ const testData = [
+ [
+ void 0,
+ {test: 'default'}
+ ],
+ [
+ null,
+ {test: 'default'}
+ ],
+ [
+ 0,
+ {test: 'default'}
+ ],
+ [
+ '',
+ {test: 'default'}
+ ],
+ [
+ [],
+ {test: 'default'}
+ ],
+ [
+ {},
+ {test: 'default'}
+ ],
+ [
+ {test: 'value'},
+ {test: 'value'}
+ ],
+ [
+ {test2: 'value2'},
+ {test: 'default'}
+ ],
+ [
+ {test: 'value', test2: 'value2'},
+ {test: 'value'}
+ ]
+ ];
+
+ for (const [value, expected] of testData) {
+ const actual = JsonSchema.getValidValueOrDefault(schema, value);
+ assert.deepStrictEqual(actual, expected);
+ }
+}
+
+function testGetValidValueOrDefault2() {
+ // Test value defaulting on objects with additionalProperties=true
+ const schema = {
+ type: 'object',
+ required: ['test'],
+ properties: {
+ test: {
+ type: 'string',
+ default: 'default'
+ }
+ },
+ additionalProperties: true
+ };
+
+ const testData = [
+ [
+ {},
+ {test: 'default'}
+ ],
+ [
+ {test: 'value'},
+ {test: 'value'}
+ ],
+ [
+ {test2: 'value2'},
+ {test: 'default', test2: 'value2'}
+ ],
+ [
+ {test: 'value', test2: 'value2'},
+ {test: 'value', test2: 'value2'}
+ ]
+ ];
+
+ for (const [value, expected] of testData) {
+ const actual = JsonSchema.getValidValueOrDefault(schema, value);
+ assert.deepStrictEqual(actual, expected);
+ }
+}
+
+function testGetValidValueOrDefault3() {
+ // Test value defaulting on objects with additionalProperties={schema}
+ const schema = {
+ type: 'object',
+ required: ['test'],
+ properties: {
+ test: {
+ type: 'string',
+ default: 'default'
+ }
+ },
+ additionalProperties: {
+ type: 'number',
+ default: 10
+ }
+ };
+
+ const testData = [
+ [
+ {},
+ {test: 'default'}
+ ],
+ [
+ {test: 'value'},
+ {test: 'value'}
+ ],
+ [
+ {test2: 'value2'},
+ {test: 'default', test2: 10}
+ ],
+ [
+ {test: 'value', test2: 'value2'},
+ {test: 'value', test2: 10}
+ ],
+ [
+ {test2: 2},
+ {test: 'default', test2: 2}
+ ],
+ [
+ {test: 'value', test2: 2},
+ {test: 'value', test2: 2}
+ ],
+ [
+ {test: 'value', test2: 2, test3: null},
+ {test: 'value', test2: 2, test3: 10}
+ ],
+ [
+ {test: 'value', test2: 2, test3: void 0},
+ {test: 'value', test2: 2, test3: 10}
+ ]
+ ];
+
+ for (const [value, expected] of testData) {
+ const actual = JsonSchema.getValidValueOrDefault(schema, value);
+ assert.deepStrictEqual(actual, expected);
+ }
+}
+
+
+function main() {
+ testValidate1();
+ testGetValidValueOrDefault1();
+ testGetValidValueOrDefault2();
+ testGetValidValueOrDefault3();
+}
+
+
+if (require.main === module) { main(); }
diff --git a/test/yomichan-test.js b/test/yomichan-test.js
new file mode 100644
index 00000000..78bfb9c6
--- /dev/null
+++ b/test/yomichan-test.js
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2020 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * 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/>.
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+
+let JSZip = null;
+
+function requireScript(fileName, exportNames, variables) {
+ const absoluteFileName = path.join(__dirname, '..', fileName);
+ const source = fs.readFileSync(absoluteFileName, {encoding: 'utf8'});
+ const exportNamesString = Array.isArray(exportNames) ? exportNames.join(',') : '';
+ const variablesArgumentName = '__variables__';
+ let variableString = '';
+ if (typeof variables === 'object' && variables !== null) {
+ variableString = Object.keys(variables).join(',');
+ variableString = `const {${variableString}} = ${variablesArgumentName};`;
+ }
+ return Function(variablesArgumentName, `'use strict';${variableString}${source}\n;return {${exportNamesString}};`)(variables);
+}
+
+function getJSZip() {
+ if (JSZip === null) {
+ process.noDeprecation = true; // Suppress a warning about JSZip
+ JSZip = require(path.join(__dirname, '../ext/mixed/lib/jszip.min.js'));
+ process.noDeprecation = false;
+ }
+ return JSZip;
+}
+
+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(dictionaryDirectory, fileName), {encoding: 'utf8'});
+ const json = JSON.parse(source);
+ if (fileName === 'index.json' && typeof dictionaryName === 'string') {
+ json.title = dictionaryName;
+ }
+ archive.file(fileName, JSON.stringify(json, null, 0));
+ }
+
+ return archive;
+}
+
+
+module.exports = {
+ requireScript,
+ createTestDictionaryArchive,
+ get JSZip() { return getJSZip(); }
+};