From 7a51a0fbde445e346a83982dd6a16ce164521e26 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Mon, 2 Mar 2020 22:20:47 -0500 Subject: [PATCH] Abstract Yomichan extension script execution --- test/dictionary-validate.js | 8 +- test/schema-validate.js | 6 +- test/test-database.js | 42 ++++----- test/test-document.js | 28 +++--- test/test-schema.js | 12 +-- test/yomichan-test.js | 13 --- test/yomichan-vm.js | 174 ++++++++++++++++++++++++++++++++++++ 7 files changed, 225 insertions(+), 58 deletions(-) create mode 100644 test/yomichan-vm.js diff --git a/test/dictionary-validate.js b/test/dictionary-validate.js index 14eee2ed..6496f2ac 100644 --- a/test/dictionary-validate.js +++ b/test/dictionary-validate.js @@ -18,10 +18,12 @@ const fs = require('fs'); const path = require('path'); -const yomichanTest = require('./yomichan-test'); +const {JSZip} = require('./yomichan-test'); +const {VM} = require('./yomichan-vm'); -const JSZip = yomichanTest.JSZip; -const {JsonSchema} = yomichanTest.requireScript('ext/bg/js/json-schema.js', ['JsonSchema']); +const vm = new VM(); +vm.execute('bg/js/json-schema.js'); +const JsonSchema = vm.get('JsonSchema'); function readSchema(relativeFileName) { diff --git a/test/schema-validate.js b/test/schema-validate.js index a4f2d94c..eb31aa8d 100644 --- a/test/schema-validate.js +++ b/test/schema-validate.js @@ -17,9 +17,11 @@ */ const fs = require('fs'); -const yomichanTest = require('./yomichan-test'); +const {VM} = require('./yomichan-vm'); -const {JsonSchema} = yomichanTest.requireScript('ext/bg/js/json-schema.js', ['JsonSchema']); +const vm = new VM(); +vm.execute('bg/js/json-schema.js'); +const JsonSchema = vm.get('JsonSchema'); function main() { diff --git a/test/test-database.js b/test/test-database.js index 35f22523..fcac9a4d 100644 --- a/test/test-database.js +++ b/test/test-database.js @@ -21,6 +21,7 @@ const url = require('url'); const path = require('path'); const assert = require('assert'); const yomichanTest = require('./yomichan-test'); +const {VM} = require('./yomichan-vm'); require('fake-indexeddb/auto'); const chrome = { @@ -88,23 +89,24 @@ class XMLHttpRequest { } } -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} = yomichanTest.requireScript('ext/mixed/js/core.js', ['stringReverse'], {chrome}); -const {requestJson} = yomichanTest.requireScript('ext/bg/js/request.js', ['requestJson'], {XMLHttpRequest}); -const databaseGlobals = { +const vm = new VM({ chrome, - JsonSchema, - requestJson, - stringReverse, - dictFieldSplit, - dictTagSanitize, + XMLHttpRequest, indexedDB: global.indexedDB, + IDBKeyRange: global.IDBKeyRange, JSZip: yomichanTest.JSZip -}; -databaseGlobals.window = databaseGlobals; -const {Database} = yomichanTest.requireScript('ext/bg/js/database.js', ['Database'], databaseGlobals); +}); +vm.context.window = vm.context; + +vm.execute([ + 'bg/js/json-schema.js', + 'bg/js/dictionary.js', + 'mixed/js/core.js', + 'bg/js/request.js', + 'bg/js/database.js' +]); +const Database = vm.get('Database'); function countTermsWithExpression(terms, expression) { @@ -212,20 +214,20 @@ async function testDatabase1() { }, {prefixWildcardsSupported: true} ); - assert.deepStrictEqual(errors, []); - assert.deepStrictEqual(result, expectedSummary); + vm.assert.deepStrictEqual(errors, []); + vm.assert.deepStrictEqual(result, expectedSummary); assert.ok(progressEvent); // Get info summary const info = await database.getDictionaryInfo(); - assert.deepStrictEqual(info, [expectedSummary]); + vm.assert.deepStrictEqual(info, [expectedSummary]); // Get counts const counts = await database.getDictionaryCounts( info.map((v) => v.title), true ); - assert.deepStrictEqual(counts, { + vm.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} }); @@ -248,10 +250,10 @@ async function testDatabase1() { async function testDatabaseEmpty1(database) { const info = await database.getDictionaryInfo(); - assert.deepStrictEqual(info, []); + vm.assert.deepStrictEqual(info, []); const counts = await database.getDictionaryCounts([], true); - assert.deepStrictEqual(counts, { + vm.assert.deepStrictEqual(counts, { counts: [], total: {kanji: 0, kanjiMeta: 0, terms: 0, termMeta: 0, tagMeta: 0} }); @@ -824,7 +826,7 @@ async function testFindTagForTitle1(database, title) { for (const {inputs, expectedResults} of data) { for (const {name} of inputs) { const result = await database.findTagForTitle(name, title); - assert.deepStrictEqual(result, expectedResults.value); + vm.assert.deepStrictEqual(result, expectedResults.value); } } } diff --git a/test/test-document.js b/test/test-document.js index edf6bbea..ab5f5716 100644 --- a/test/test-document.js +++ b/test/test-document.js @@ -2,7 +2,7 @@ const fs = require('fs'); const path = require('path'); const assert = require('assert'); const {JSDOM} = require('jsdom'); -const yomichanTest = require('./yomichan-test'); +const {VM} = require('./yomichan-vm'); // DOMRect class definition @@ -74,20 +74,18 @@ async function testDocument1() { const Node = window.Node; const Range = window.Range; - const {DOM} = yomichanTest.requireScript( - 'ext/mixed/js/dom.js', - ['DOM'] - ); - const {TextSourceRange, TextSourceElement} = yomichanTest.requireScript( - 'ext/fg/js/source.js', - ['TextSourceRange', 'TextSourceElement'], - {document, window, Range, Node} - ); - const {docRangeFromPoint, docSentenceExtract} = yomichanTest.requireScript( - 'ext/fg/js/document.js', - ['docRangeFromPoint', 'docSentenceExtract'], - {document, window, Node, TextSourceElement, TextSourceRange, DOM} - ); + const vm = new VM({document, window, Range, Node}); + vm.execute([ + 'mixed/js/dom.js', + 'fg/js/source.js', + 'fg/js/document.js' + ]); + const [TextSourceRange, TextSourceElement, docRangeFromPoint, docSentenceExtract] = vm.get([ + 'TextSourceRange', + 'TextSourceElement', + 'docRangeFromPoint', + 'docSentenceExtract' + ]); try { await testDocumentTextScanningFunctions(dom, {docRangeFromPoint, docSentenceExtract, TextSourceRange, TextSourceElement}); diff --git a/test/test-schema.js b/test/test-schema.js index f4612f86..5f9915fd 100644 --- a/test/test-schema.js +++ b/test/test-schema.js @@ -17,9 +17,11 @@ */ const assert = require('assert'); -const yomichanTest = require('./yomichan-test'); +const {VM} = require('./yomichan-vm'); -const {JsonSchema} = yomichanTest.requireScript('ext/bg/js/json-schema.js', ['JsonSchema']); +const vm = new VM(); +vm.execute('bg/js/json-schema.js'); +const JsonSchema = vm.get('JsonSchema'); function testValidate1() { @@ -138,7 +140,7 @@ function testGetValidValueOrDefault1() { for (const [value, expected] of testData) { const actual = JsonSchema.getValidValueOrDefault(schema, value); - assert.deepStrictEqual(actual, expected); + vm.assert.deepStrictEqual(actual, expected); } } @@ -177,7 +179,7 @@ function testGetValidValueOrDefault2() { for (const [value, expected] of testData) { const actual = JsonSchema.getValidValueOrDefault(schema, value); - assert.deepStrictEqual(actual, expected); + vm.assert.deepStrictEqual(actual, expected); } } @@ -235,7 +237,7 @@ function testGetValidValueOrDefault3() { for (const [value, expected] of testData) { const actual = JsonSchema.getValidValueOrDefault(schema, value); - assert.deepStrictEqual(actual, expected); + vm.assert.deepStrictEqual(actual, expected); } } diff --git a/test/yomichan-test.js b/test/yomichan-test.js index 78bfb9c6..0fc97b4b 100644 --- a/test/yomichan-test.js +++ b/test/yomichan-test.js @@ -22,18 +22,6 @@ 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) { @@ -64,7 +52,6 @@ function createTestDictionaryArchive(dictionary, dictionaryName) { module.exports = { - requireScript, createTestDictionaryArchive, get JSZip() { return getJSZip(); } }; diff --git a/test/yomichan-vm.js b/test/yomichan-vm.js new file mode 100644 index 00000000..ff478844 --- /dev/null +++ b/test/yomichan-vm.js @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2020 Alex Yatskov + * Author: Alex Yatskov + * + * 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 . + */ + +const fs = require('fs'); +const vm = require('vm'); +const path = require('path'); +const assert = require('assert'); + + +function getContextEnvironmentRecords(context, names) { + // Enables export of values from the declarative environment record + if (!Array.isArray(names) || names.length === 0) { + return []; + } + + let scriptSource = '(() => {\n "use strict";\n const results = [];'; + for (const name of names) { + scriptSource += `\n try { results.push(${name}); } catch (e) { results.push(void 0); }`; + } + scriptSource += '\n return results;\n})();'; + + const script = new vm.Script(scriptSource, {filename: 'getContextEnvironmentRecords'}); + + const contextHasNames = Object.prototype.hasOwnProperty.call(context, 'names'); + const contextNames = context.names; + context.names = names; + + const results = script.runInContext(context, {}); + + if (contextHasNames) { + context.names = contextNames; + } else { + delete context.names; + } + + return Array.from(results); +} + +function isDeepStrictEqual(val1, val2) { + if (val1 === val2) { return true; } + + if (Array.isArray(val1)) { + if (Array.isArray(val2)) { + return isArrayDeepStrictEqual(val1, val2); + } + } else if (typeof val1 === 'object' && val1 !== null) { + if (typeof val2 === 'object' && val2 !== null) { + return isObjectDeepStrictEqual(val1, val2); + } + } + + return false; +} + +function isArrayDeepStrictEqual(val1, val2) { + const ii = val1.length; + if (ii !== val2.length) { return false; } + + for (let i = 0; i < ii; ++i) { + if (!isDeepStrictEqual(val1[i], val2[i])) { + return false; + } + } + + return true; +} + +function isObjectDeepStrictEqual(val1, val2) { + const keys1 = Object.keys(val1); + const keys2 = Object.keys(val2); + + if (keys1.length !== keys2.length) { return false; } + + const keySet = new Set(keys1); + for (const key of keys2) { + if (!keySet.delete(key)) { return false; } + } + + for (const key of keys1) { + if (!isDeepStrictEqual(val1[key], val2[key])) { + return false; + } + } + + const tag1 = Object.prototype.toString.call(val1); + const tag2 = Object.prototype.toString.call(val2); + if (tag1 !== tag2) { return false; } + + return true; +} + +function deepStrictEqual(actual, expected) { + try { + // This will fail on prototype === comparison on cross context objects + assert.deepStrictEqual(actual, expected); + } catch (e) { + if (!isDeepStrictEqual(actual, expected)) { + throw e; + } + } +} + + +class VM { + constructor(context={}) { + this._context = vm.createContext(context); + this._assert = { + deepStrictEqual + }; + } + + get context() { + return this._context; + } + + get assert() { + return this._assert; + } + + get(names) { + if (typeof names === 'string') { + return getContextEnvironmentRecords(this._context, [names])[0]; + } else if (Array.isArray(names)) { + return getContextEnvironmentRecords(this._context, names); + } else { + throw new Error('Invalid argument'); + } + } + + set(values) { + if (typeof values === 'object' && values !== null) { + Object.assign(this._context, values); + } else { + throw new Error('Invalid argument'); + } + } + + execute(fileNames) { + const single = !Array.isArray(fileNames); + if (single) { + fileNames = [fileNames]; + } + + const results = []; + for (const fileName of fileNames) { + const absoluteFileName = path.resolve(__dirname, '..', 'ext', fileName); + const source = fs.readFileSync(absoluteFileName, {encoding: 'utf8'}); + const script = new vm.Script(source, {filename: absoluteFileName}); + results.push(script.runInContext(this._context, {})); + } + + return single ? results[0] : results; + } +} + + +module.exports = { + VM +};