diff --git a/src/code-transforms.js b/src/code-transforms.js index 6c01f32..fa146e5 100644 --- a/src/code-transforms.js +++ b/src/code-transforms.js @@ -1,5 +1,4 @@ const Babel = require('@babel/standalone'); -const generate = require('@babel/generator'); exports.toES2015 = toES2015; exports.prepare = prepare; @@ -68,8 +67,7 @@ function stepInjector(babel) { path.node.body = t.blockStatement([ createContextCall( babel, - 'step', - generate.default(path.node).code + 'step' ) ]); path.skip(); @@ -116,22 +114,19 @@ function stepInjector(babel) { }; } -function createContextCall(babel, fnName, expr) { +function createContextCall(babel, fnName) { const { types: t } = babel; const stepperName = t.identifier(fnName); - const stepperArgs = [ - t.templateLiteral([t.templateElement({ raw: expr })], []) - ]; return t.expressionStatement( - t.awaitExpression(t.callExpression(stepperName, stepperArgs)) + t.awaitExpression(t.callExpression(stepperName, [])) ); } function prependContextCall(babel, path) { return path.insertBefore( - createContextCall(babel, 'step', generate.default(path.node).code) + createContextCall(babel, 'step') ); } @@ -146,8 +141,7 @@ function implicitToExplicitReturnFunction(babel, path) { const stepCall = createContextCall( babel, - 'step', - generate.default(path.node.body).code + 'step' ); const returnStatement = t.returnStatement(path.node.body); const body = t.blockStatement([stepCall, returnStatement]); diff --git a/src/context.js b/src/context.js index cfe9d2f..ef961c2 100644 --- a/src/context.js +++ b/src/context.js @@ -23,7 +23,6 @@ const createContext = ({ events, userContext, stepper }) => { }, pause: () => stepper.pause(), resume: () => stepper.resume(), - setStepTime: (ms) => stepper.setStepTime(ms), activeHandlers }, ...userContext diff --git a/src/execution-controller.js b/src/execution-controller.js index 1fec848..7e5efc5 100644 --- a/src/execution-controller.js +++ b/src/execution-controller.js @@ -4,7 +4,6 @@ const createExecutionController = ({ execution, events, context }) => { stop, pause, resume, - setStepTime } = context._execution; const controller = { @@ -15,7 +14,6 @@ const createExecutionController = ({ execution, events, context }) => { stop, pause, resume, - setStepTime, context, promises: { get executionEnd() { diff --git a/src/interpreter.js b/src/interpreter.js index d3d3de0..e5c5e40 100644 --- a/src/interpreter.js +++ b/src/interpreter.js @@ -8,7 +8,6 @@ const Stepper = require('./stepper'); const run = (code = '', options = {}) => { const { - stepTime = 15, on = {}, context: userContext = {}, es2015 = false, @@ -17,7 +16,7 @@ const run = (code = '', options = {}) => { } = options; const events = EventEmitter(); - const stepper = new Stepper({ stepTime }); + const stepper = new Stepper(); const context = createContext({ events, userContext, stepper }); const vm = new VM(context); diff --git a/src/stepper.js b/src/stepper.js index bd39959..827ec86 100644 --- a/src/stepper.js +++ b/src/stepper.js @@ -1,20 +1,17 @@ const EventEmitter = require('./event-emitter'); class Stepper { - constructor(options = {}) { - const { stepTime = 100 } = options; - + constructor() { this.events = EventEmitter(); - this.stepTime = stepTime; } - async step(expr) { + async step(ms) { if (this.destroyed) { throw 'stepper-destroyed'; } - this.events.emit('step', expr); - this.currentStep = wait(this.stepTime); + this.events.emit('step'); + this.currentStep = ms ? wait(ms) : undefined; try { await this.currentStep; @@ -56,10 +53,6 @@ class Stepper { return this.events.once(event, handler); } - setStepTime(stepTime) { - this.stepTime = stepTime; - } - async destroy() { this.events.destroy(); this.destroyed = true; @@ -76,7 +69,7 @@ class Stepper { module.exports = Stepper; -function wait(ms = false) { +function wait(ms) { let rejector; let resolver; @@ -84,13 +77,27 @@ function wait(ms = false) { rejector = reject; resolver = resolve; - if (ms !== false) { - setTimeout(resolve, ms); + if (ms && ms !== Infinity) { + setTimeout(() => promise.resolve(), ms); } }); - promise.cancel = () => rejector('destroyed'); - promise.resolve = () => resolver(); + const destroy = () => { + rejector = null; + resolver = null; + }; + promise.cancel = () => { + if (rejector) { + rejector('destroyed'); + } + destroy(); + }; + promise.resolve = () => { + if (resolver) { + resolver(); + } + destroy(); + }; return promise; } diff --git a/test/code-transforms.test.js b/test/code-transforms.test.js index 5b792b6..5b9ade2 100644 --- a/test/code-transforms.test.js +++ b/test/code-transforms.test.js @@ -63,27 +63,27 @@ describe('code-transforms', function () { describe('step injection', function () { it('should inject steps before declarations', function () { const input = `const a = 1;`; - const step = 'await step(`const a = 1;`);'; + const step = 'await step();'; expect(prepare(input)).to.include(step); }); it('should inject steps before declarations inside functions', function () { const input = `function test() { const a = 1; }`; - const step = 'await step(`const a = 1;`);'; + const step = 'await step();'; expect(prepare(input)).to.include(step); }); it('should inject steps before declarations inside for loops', function () { const input = `for(let i = 0; i < 1; i++) { const a = 1; }`; - const step = 'await step(`const a = 1;`);'; + const step = 'await step();'; expect(prepare(input)).to.include(step); }); it('should inject steps before declarations inside while loops', function () { const input = `while(true) { const a = 1; }`; - const step = 'await step(`const a = 1;`);'; + const step = 'await step();'; expect(prepare(input)).to.include(step); }); - it('should inject step call with the next block code as argument (without that block steps)', function () { + it('should inject multiple steps', function () { const input = `while (true) { console.log('hey!'); }`; - const output = /await step\(`while \(true\) {\s+console\.log\('hey!'\);\s+}`\)/s; + const output = /await step\(\)/s; expect(prepare(input)).to.match(output); }); it('should inject step calls inside anonymous callback functions', async function () { @@ -93,7 +93,7 @@ describe('code-transforms', function () { console.log(element); }); `; - const step = 'await step(`console.log(element);`)'; + const step = 'await step()'; expect(prepare(input)).to.include(step); }); it('should inject step calls inside arrow functions with body', async function () { @@ -105,8 +105,8 @@ describe('code-transforms', function () { }); `; - const step1 = 'await step(`console.log(element);`)'; - const step2 = 'await step(`return element + 1;`)'; + const step1 = 'await step()'; + const step2 = 'await step()'; expect(prepare(input)).to.include(step1); expect(prepare(input)).to.include(step2); }); @@ -116,7 +116,7 @@ describe('code-transforms', function () { const b = a.map(element => element + 1); `; - const step = 'await step(`element + 1`);'; + const step = 'await step();'; expect(prepare(input)).to.include(step); }); it('should inject step calls inside with blocks', async function () { @@ -126,7 +126,7 @@ describe('code-transforms', function () { } `; - const step = 'await step(`const a = [1, 2];`);'; + const step = 'await step();'; expect(prepare(input)).to.include(step); }); }); diff --git a/test/interpreter.test.js b/test/interpreter.test.js index 8f3e393..e5033a8 100644 --- a/test/interpreter.test.js +++ b/test/interpreter.test.js @@ -36,7 +36,6 @@ describe('interpreter', function () { 'stop', 'resume', 'promises', - 'setStepTime', 'on', 'emit', 'once', @@ -62,6 +61,7 @@ describe('interpreter', function () { it('run() promises should be fulfilled when user stops execution', async function () { const code = ` while(true) { + step(1); const a = 1; } `; @@ -101,7 +101,7 @@ describe('interpreter', function () { const execution = run(code); const { promises } = execution; const { executionEnd } = promises; - setTimeout(() => execution.pause(), 10); + execution.pause(); setTimeout(() => execution.resume(), 200); await expect(execution).to.eventually.be.fulfilled; await expect(executionEnd).to.eventually.be.fulfilled; @@ -152,9 +152,9 @@ describe('interpreter', function () { expect(callback).to.have.been.callCount(5); }); - it('should be protected against infinite while loops', async function () { + it('should be protected against infinite while loops with explicit step', async function () { const code = ` - while(true) {} + while(true) { step(1); } `; const execution = run(code); @@ -162,9 +162,9 @@ describe('interpreter', function () { await expect(execution).to.eventually.be.fulfilled; }); - it('should be protected against infinite for loops', async function () { + it('should be protected against infinite for loops with explicit step', async function () { const code = ` - for (;;) {} + for (;;) { step(1); } `; const execution = run(code); @@ -172,24 +172,6 @@ describe('interpreter', function () { await expect(execution).to.eventually.be.fulfilled; }); - it('should be able to control step time while running', async function () { - const INITIAL_STEP_TIME = 100; - - const code = ` - for (let i = 0; i < 2; i++) { - const a = 1; - } - `; - - const before = performance.now(); - const execution = run(code, { stepTime: INITIAL_STEP_TIME }); - setTimeout(() => execution.setStepTime(1), 10); - await expect(execution).to.eventually.be.fulfilled; - const after = performance.now(); - - expect(before - after).to.be.lessThan(INITIAL_STEP_TIME * 2); - }); - it('should be able to execute code in parallel', async function () { const logger = sinon.fake(); const parallel = sinon.spy((...fns) => @@ -280,12 +262,12 @@ describe('interpreter', function () { await run(code, { on: { start: callback } }); expect(callback).to.have.been.called; }); - it('should fire on.step event with next expression', async function () { + it('should fire on.step event', async function () { const callback = sinon.fake(); const code = `const firstStep = 1;`; await run(code, { on: { step: callback } }); - expect(callback).to.have.been.calledWith('const firstStep = 1;'); + expect(callback).to.have.been.called; }); it('should call on.step event 2 times', async function () { const callback = sinon.fake(); diff --git a/test/stepper.test.js b/test/stepper.test.js index cb2f5a1..5cd6a02 100644 --- a/test/stepper.test.js +++ b/test/stepper.test.js @@ -17,51 +17,23 @@ describe('stepper', function () { }); }); describe('stepping', function () { - it('should resolve in configured step time', async function () { - const stepTime = 50; - // rename this variable - const allowedDifferenceMs = 10; - const stepper = new Stepper({ stepTime }); - - const before = performance.now(); - await stepper.step(); - const after = performance.now(); - - expect(after - before).to.be.greaterThan( - stepTime - allowedDifferenceMs - ); - expect(after - before).to.be.lessThan( - stepTime + allowedDifferenceMs - ); - }); - it('should be able to be destroyed', async function () { - const stepTime = 200; - const stepper = new Stepper({ stepTime }); + const stepper = new Stepper(); - setTimeout(() => stepper.destroy(), 10); - const before = performance.now(); + stepper.destroy(); await expect(stepper.step()).to.be.eventually.rejectedWith( 'stepper-destroyed' ); - const after = performance.now(); - - expect(after - before).to.be.lessThan(stepTime); }); it('should not be able to step again after being destroyed', async function () { - const stepTime = 200; - const stepper = new Stepper({ stepTime }); + const stepper = new Stepper(); - setTimeout(() => stepper.destroy(), 10); + stepper.destroy(); - const before = performance.now(); await expect(stepper.step()).to.be.eventually.rejectedWith( 'stepper-destroyed' ); - const after = performance.now(); - expect(after - before).to.be.lessThan(stepTime); - await expect(stepper.step()).to.be.eventually.rejectedWith( 'stepper-destroyed' ); @@ -71,33 +43,56 @@ describe('stepper', function () { }); it('should be able to be paused', async function () { - // pausing the stepper for a bigger time than its step time - // should be enough to understand if it has indeed been paused - // we use a small stepTime and a big pauseTime and make sure - // the actual step time was bigger than both step time and pausedTime - const stepTime = 20; - const stepper = new Stepper({ stepTime }); + const stepper = new Stepper(); - // rename this variable - const pauseAtMs = 10; const pauseForMs = 100; - setTimeout(() => stepper.pause(), pauseAtMs); - setTimeout(() => stepper.resume(), pauseAtMs + pauseForMs); + // step is immediate + stepper.pause(); + setTimeout(() => stepper.resume(), 110); const before = performance.now(); await expect(stepper.step()).to.be.eventually.fulfilled; const after = performance.now(); - expect(after - before).to.be.greaterThan(stepTime); expect(after - before).to.be.greaterThan(pauseForMs); }); + it('should be able to call step with Infinity', async function () { + const stepper = new Stepper(); + + setTimeout(() => stepper.destroy(), 200); + await expect(stepper.step(Infinity)).to.be.rejectedWith( + 'stepper-destroyed' + ); + }); + + it('should be able to call step with 0 and resolve immediately', async function () { + const stepper = new Stepper(); + + const before = performance.now(); + await expect(stepper.step(0)).to.be.eventually.fulfilled; + const after = performance.now(); + + expect(after - before).to.be.lessThan(1); + }); + + it('should be able to call step with 100ms', async function () { + const stepper = new Stepper(); + const threshold = 100; + + const before = performance.now(); + await expect(stepper.step(threshold + 10)).to.be.eventually.fulfilled; + const after = performance.now(); + + expect(after - before).to.be.greaterThan(threshold); + }); + it('should be able to be destroyed while paused', async function () { - const stepTime = 50; - const stepper = new Stepper({ stepTime }); + const stepper = new Stepper(); - setTimeout(() => stepper.pause(), 10); + // step is immediate + stepper.pause(); setTimeout(() => stepper.destroy(), 100); const before = performance.now(); @@ -106,18 +101,18 @@ describe('stepper', function () { ); const after = performance.now(); - expect(after - before).to.be.greaterThan(stepTime); + expect(after - before).to.be.greaterThan(100); }); it('pausing multiple times should have no effect', async function () { - const stepTime = 20; - const stepper = new Stepper({ stepTime }); + const stepper = new Stepper(); // rename this variable const pauseAtMs = 10; const pauseForMs = 100; - setTimeout(() => stepper.pause(), pauseAtMs); + // step is immediate + stepper.pause(); setTimeout(() => stepper.pause(), pauseAtMs + 5); setTimeout(() => stepper.pause(), pauseAtMs + 10); setTimeout(() => stepper.resume(), pauseAtMs + pauseForMs); @@ -126,19 +121,18 @@ describe('stepper', function () { await expect(stepper.step()).to.be.eventually.fulfilled; const after = performance.now(); - expect(after - before).to.be.greaterThan(stepTime); expect(after - before).to.be.greaterThan(pauseForMs); }); it('resuming multiple times should have no effect', async function () { - const stepTime = 20; - const stepper = new Stepper({ stepTime }); + const stepper = new Stepper(); // rename this variable const pauseAtMs = 10; const pauseForMs = 100; - setTimeout(() => stepper.pause(), pauseAtMs); + // step is immediate + stepper.pause(); setTimeout(() => stepper.resume(), pauseAtMs + pauseForMs); setTimeout(() => stepper.resume(), pauseAtMs + pauseForMs + 1); setTimeout(() => stepper.resume(), pauseAtMs + pauseForMs + 2); @@ -147,20 +141,18 @@ describe('stepper', function () { await expect(stepper.step()).to.be.eventually.fulfilled; const after = performance.now(); - expect(after - before).to.be.greaterThan(stepTime); expect(after - before).to.be.greaterThan(pauseForMs); }); }); describe('events', function () { - it('should emit step event with arguments', async function () { - const stepTime = 5; - const stepper = new Stepper({ stepTime }); + it('should emit step event', async function () { + const stepper = new Stepper(); const callback = sinon.fake(); stepper.on('step', callback); - await stepper.step('expression'); - expect(callback).to.have.been.calledWith('expression'); + await stepper.step(); + expect(callback).to.have.been.called; }); }); });