aboutsummaryrefslogtreecommitdiff
path: root/test/yomichan-vm.js
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2020-03-04 21:24:39 -0500
committerGitHub <noreply@github.com>2020-03-04 21:24:39 -0500
commit421b60db0f6f132cbc5993488041846055a76d11 (patch)
treeb2b28efc8ed7651c63ca2e728096ab79c6e6ab06 /test/yomichan-vm.js
parent6e47b4e241594d68384f62b1faa34c86f85addbf (diff)
parent7a51a0fbde445e346a83982dd6a16ce164521e26 (diff)
Merge pull request #387 from toasted-nutbread/yomichan-vm
Abstract Yomichan extension script execution
Diffstat (limited to 'test/yomichan-vm.js')
-rw-r--r--test/yomichan-vm.js174
1 files changed, 174 insertions, 0 deletions
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 <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 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
+};