UNPKG

lively.vm

Version:

Controlled JavaScript code execution and instrumentation.

384 lines (310 loc) 12.7 kB
/*global beforeEach, afterEach, describe, it, global,System,xdescribe*/ import { expect } from "mocha-es6"; import { runEval, syncEval, defaultTopLevelVarRecorderName } from "../index.js"; import * as lang from "lively.lang"; var Global = typeof global !== "undefined" ? global : window; describe("evaluation", function() { it("syncEval", function() { expect(syncEval('1 + 2')).property("value").equals(3); expect(syncEval('this.foo + 2;', {context: {foo: 3}})).property("value").equals(5); expect(syncEval('throw new Error("foo");')) .to.containSubset({isError: true}) .property("value").matches(/Error.*foo/); }); it("runEval", () => runEval('1+2') .then(result => expect(result.value).equal(3)) .catch(err => expect(false, "got error in runEval " + err))); it("runEval promise-rejects on error", () => runEval('throw new Error("foo");') .then(result => { expect(result).property("isError").to.be.true; expect(result).property("value").to.match(/error.*foo/i) }) .catch(err => expect(false, "got error in runEval " + err))) it("eval and capture topLevelVars", function() { var varMapper = {}, code = "var x = 3 + 2", result = syncEval(code, {topLevelVarRecorder: varMapper}); expect(result.value).equals(5); expect(varMapper.x).equals(5); }); it("eval single expressions", function() { var result = syncEval('function() {}'); expect(result.value).to.be.a("function"); var result = syncEval('{x: 3}'); expect(result.value).to.an('object'); expect(result.value.x).equals(3); }); it("evalCapturedVarsReplaceVarRefs", function() { var varMapper = {}; var code = "var x = 3; var y = foo() + x; function foo() { return x++ }; y"; var result = syncEval(code, {topLevelVarRecorder: varMapper}); expect(result.value).equals(7); expect(varMapper.x).equals(4); expect(varMapper.y).equals(7); }); it("only capture whitelisted globals", function() { var varMapper = {y: undefined}, code = "var x = 3; y = 5; z = 4;"; syncEval(code, {topLevelVarRecorder: varMapper}); expect(varMapper.x).equals(3); expect(varMapper.y).equals(5); expect(!varMapper.hasOwnProperty('z')).equals(true, 'Global "z" was recorded'); // don't leave globals laying around delete Global.z; }); it("dont capture blacklisted", function() { var varMapper = {}, code = "var x = 3, y = 5;"; syncEval(code, { topLevelVarRecorder: varMapper, dontTransform: ['y'] }); expect(varMapper.x).equals(3); expect(!varMapper.hasOwnProperty('y')).equals(true, 'y recorded?'); }); it("dont transform var decls in for loop", function() { var code = "var sum = 0;\n" + "for (var i = 0; i < 5; i ++) { sum += i; }\n" + "sum", recorder = {}; syncEval(code, {topLevelVarRecorder: recorder}); expect(recorder.sum).equals(10); }); it("eval captured vars replace var refs", function() { var code = 'try { throw new Error("foo") } catch (e) { e.message }', result = syncEval(code, {topLevelVarRecorder: {}}); expect(result.value).equals('foo'); }); it("dont undefine vars", function() { var code = "var x; var y = x + 3;", rec = {x: 23}; syncEval(code, {topLevelVarRecorder: rec}); expect(rec.y).equals(26); }); it("eval can set source url", function() { if ((System.get("@system-env").node) || navigator.userAgent.match(/PhantomJS/)) { console.log("FIXME sourceURL currently only works for web browser"); return; } var code = "throw new Error('test');", result = syncEval(code, {sourceURL: "my-great-source!"}); expect(result.value.stack).to.match( /at eval.*my-great-source!:1/, "stack does not have sourceURL info:\n" + lang.string.lines(result.value.stack).slice(0,3).join('\n')); }); describe("promises", () => { it("runEval returns promise", () => runEval("3+5").then(result => expect(result).property("value").to.equal(8))); var code = "new Promise(function(resolve, reject) { return setTimeout(resolve, 200, 23); });" it("run eval waits for promise", () => runEval(code, {waitForPromise: true}) .then(result => expect(result).to.containSubset({ isPromise: true, promiseStatus: "fulfilled", promisedValue: 23}))); }); describe("printed", () => { it("asString", () => runEval("3 + 4", {asString: true}) .then(printed => expect(printed).to.containSubset({value: "7"}))); it("inspect", () => runEval("({foo: {bar: {baz: 42}, zork: 'graul'}})", {inspect: true, printDepth: 2}) .then(printed => expect(printed).to.containSubset({value: "{\n foo: {\n bar: {/*...*/},\n zork: \"graul\"\n }\n}"}))); it("prints promises", () => runEval("Promise.resolve(23)", {asString: true}) .then(printed => expect(printed).to.containSubset({value: 'Promise({status: "fulfilled", value: 23})'}))); }); describe("transpiler", () => { it("transforms code after lively rewriting", () => runEval("x + 3", { topLevelVarRecorder: {x: 2}, // ".x" will only appear in rewritten code transpiler: (source, opts) => source.replace(/\.x/, ".x + 1") }) .then(({value}) => expect(value).to.equal(6))); }); }); describe("context recording", () => { describe("record free variables", () => { it("adds them to var recorder", () => { var varMapper = {}, code = "x = 3 + 2", result = syncEval(code, {topLevelVarRecorder: varMapper, recordGlobals: true}); expect(result).property("value").equals(5); expect(varMapper.x).equals(5); }); }); }); describe("wrap in start-end calls", () => { var evalCount, evalEndRecordings, varMapper, opts; function onStartExecution() { evalCount++; } function onEndExecution(err, result) { evalEndRecordings.push({error: err, result: result}); } beforeEach(() => { evalCount = 0; evalEndRecordings = []; varMapper = {}; opts = { topLevelVarRecorder: varMapper, recordGlobals: true, wrapInStartEndCall: true, onStartEval: onStartExecution, onEndEval: onEndExecution } }); describe("async", () => { it("calls start and end handlers", () => runEval("var x = 3 + 2", opts).then(result => { expect(result).property("value").equals(5); expect(varMapper.x).equals(5); expect(evalCount).to.equal(1); expect(evalEndRecordings).to.have.length(1); expect(evalEndRecordings).to.deep.equal([{error: null, result: 5}]); })); it("works with errors", () => runEval("foo.bar()", opts).then(result => { expect(result.isError).equals(true); expect(result.value).to.match(/TypeError/); expect(evalCount).to.equal(1); expect(evalEndRecordings).to.have.length(1); expect(evalEndRecordings[0]).to.have.property("error").to.match(/TypeError/); })); }); describe("sync", () => { it("calls injected function before and after eval, using value of last expression", () => { var result = syncEval("var x = 3 + 2", opts); expect(result).property("value").equals(5); expect(varMapper.x).equals(5); expect(evalCount).to.equal(1); expect(evalEndRecordings).to.have.length(1); expect(evalEndRecordings).to.deep.equal([{error: null, result: 5}]); }); it("works with errors", () => { var result = syncEval("foo.bar()", opts); expect(result.isError).equals(true); expect(result.value).to.match(/TypeError/); expect(evalCount).to.equal(1); expect(evalEndRecordings).to.have.length(1); expect(evalEndRecordings[0]).to.have.property("error").to.match(/TypeError/); }); }); }); describe("runtime", () => { it("evaluation uses runtime", () => { var runtime = { modules: { "foo/bar.js": { topLevelVarRecorder: {}, recordGlobals: true } } } syncEval("var x = 3 + 2; y = 2", {runtime: runtime, targetModule: "foo/bar.js"}); expect(runtime.modules["foo/bar.js"].topLevelVarRecorder.x).equals(5); expect(runtime.modules["foo/bar.js"].topLevelVarRecorder.y).equals(2); }); }); describe("declaration callback", () => { it("invokes function when declaration is evaluated", async () => { var recordings = []; await runEval("var a = 1, b = 'foo'; a = 2", { topLevelVarRecorder: {}, declarationCallback: (name, kind, val, recorder) => recordings.push([name, val]) }); expect(recordings).deep.equals([["a", 1], ["b", 'foo'], ["a", 2]]) }); }); describe("persistent definitions", () => { var varMapper, opts; beforeEach(() => { varMapper = {}; opts = {topLevelVarRecorder: varMapper} }); describe("primitives", () => { it("redefines number, strings, regexp", async () => { await runEval("var a = 1, b = '2', c = /foo/", opts); await runEval("var a = 2, b = '3', c = /bar/", opts); expect(varMapper.a).equals(2, "number"); expect(varMapper.b).equals('3', "string"); expect(varMapper.c.test("bar")).equals(true, "regexp"); }); }); xdescribe("objects", () => { it("keeps identy", async () => { var result1 = await runEval("var x = {y: 23}", opts), x1 = varMapper.x; expect(result1.value).deep.equals({y: 23}); expect(x1).deep.equals({y: 23}); var result2 = await runEval("var x = {y: 24}", opts), x2 = varMapper.x; expect(result2.value).deep.equals({y: 24}, "eval result"); expect(x2).deep.equals({y: 24}, "var mapper value"); expect(x1).equals(x2, "identity"); }); it("keeps symbols", async () => { var sym = Symbol.for('y'), result1 = await runEval("var x = {[Symbol.for('y')]: 23}", opts), x1 = varMapper.x; expect(result1.value).deep.equals({[sym]: 23}); expect(x1).deep.equals({[sym]: 23}); var result2 = await runEval("var x = {[Symbol.for('y')]: 24}", opts), x2 = varMapper.x; expect(result2.value).deep.equals({[sym]: 24}); expect(x2).deep.equals({[sym]: 24}); expect(x1).equals(x2); }); it("keeps identity of generated object", async () => { await runEval("var a = (function() { return {x: 23}; })();", opts); var a1 = varMapper.a; await runEval("var a = (function() { return {x: 24}; })();", opts); var a2 = varMapper.a; expect(a2).deep.equals({x: 24}); expect(a2).equals(a1); }); it("copies getters and setters", async () => { await runEval("var a = {get x() { return this._x; }, set x(v) { this._x = v; }}; a.x = 3;", opts); var a1 = varMapper.a; expect(a1.x).equals(3); await runEval("var a = {get x() { return this._x; }, set x(v) { this._x = v + 1; }};", opts); var a2 = varMapper.a; expect(a1).equals(a2); expect(a2.x).equals(3); a2.x = 4; expect(a2.x).equals(5); }); }); describe("class", () => { beforeEach(() => runEval("class Foo {a() { return 1 } get b() { return 2 } static c() { return 3; }}", opts)) it("keeps identity", async () => { var Foo1 = varMapper.Foo; await runEval("class Foo {a() { return 2 }}", opts); var Foo2 = varMapper.Foo; expect(new Foo2().a()).equals(2); expect(Foo1).equals(Foo2); }); it("redefines props", async () => { await runEval("class Foo {get b() { return 3 }}", opts); expect(new varMapper.Foo().b).equals(3); }); it("redefines class side", async () => { await runEval("class Foo {static c() { return 4 }}", opts); expect(varMapper.Foo.c()).equals(4); }); it("class identical to instance constructor", async () => { var isIdentical = (await runEval("class Bar {}; Bar === new Bar().constructor", opts)).value; expect(isIdentical).equals(true); }); it("redefines class twice and keeps identity", async () => { var isIdentical = (await runEval("class Bar {}; class Bar {}; Bar === new Bar().constructor", opts)).value; expect(isIdentical).equals(true); }); it("class methods don't shadow similar named functions in lexical scope", async () => { expect(await runEval("function m() {return 3}; class Bar {m() { return m() }}; new Bar().m()", opts)) .property("value").equals(3); }); it("class is only captured in toplevel scope", async () => { await runEval("(function() { class InnerClass {a() { return 2 }} })()", opts); expect(varMapper).to.not.have.property("InnerClass"); }); }); });