UNPKG

plan

Version:

execute a complicated dependency graph of tasks with smooth progress events

483 lines (472 loc) 15.7 kB
var Plan = require('../') , assert = require('assert'); var SmoothTask = { start: function(done) { var self = this; self.exports.amountTotal = 30; var interval = setInterval(function() { self.exports.amountDone += 1; if (self.exports.amountDone === 30) { clearInterval(interval); self.exports.complete = true; done(); } else { self.emit('progress'); } }, 10); } }; var DelayTask = { start: function(done) { var self = this; setTimeout(function() { self.exports.amountTotal = self.options.timeout; self.emit('progress'); setTimeout(function() { done(); }, self.options.timeout); }, 10); } }; var FastTask = { exports: { foo: { bar: "abcd" }, derp: true, amountTotal: 2, }, start: function(done) { var self = this; self.exports.derp = "hi"; self.emit('update'); setTimeout(function() { self.amountDone = 1; self.emit('progress'); setTimeout(function() { self.exports.complete = true; done(); }); }, 10); }, }; var SyncTask = { start: function(done) { this.context.foo = "foo2"; this.context.tails = this.options.tails; this.exports.sonic = this.options.sonic || "zebra"; this.exports.complete = true; done(); } }; var ErrorTask = { exports: { amountTotal: 20 }, start: function(done) { var self = this; var interval = setInterval(function() { self.exports.amountDone += 1; self.emit('progress'); if (self.exports.amountDone === 10) { clearInterval(interval); var err = new Error("ErrorTask - this error is expected"); err.isErrorTask = true; done(err); } }, 10); } }; var SetNumberTask = { start: function(done) { this.context[this.options.field] = this.options.value; done(); } }; var SumTask = { start: function(done) { this.context.result = this.context.a + this.context.b; done(); } }; var counterTaskCounter = 0; var CounterTask = { start: function(done) { var self = this; counterTaskCounter += 1; self.exports.amountTotal = 30; var interval = setInterval(function() { self.exports.amountDone += 1; if (self.exports.amountDone === 30) { clearInterval(interval); self.exports.complete = true; done(); } else { self.emit('progress'); } }, 10); } }; var CpuTask = { cpuBound: true, start: function(done) { var self = this; self.exports.amountTotal = 100; var interval = setInterval(function() { self.exports.amountDone += 1; if (self.exports.amountDone === 100) { clearInterval(interval); self.exports.complete = true; done(); } else { self.emit('progress'); } }, 10); } }; describe("plan", function() { it("a single task", function(done) { var plan = new Plan("aoeuaoeu"); var task = Plan.createTask(SmoothTask); var info = task.exports; plan.addTask(task); plan.on('error', done); var progress = 0; var progressEventCount = 0; plan.on('progress', function(amountDone, amountTotal) { var newProgress = amountDone / amountTotal; assert(newProgress >= progress, "old progress: " + progress + ", new progress: " + newProgress); progressEventCount += 1; progress = newProgress; }); var updateEventCount = 0; plan.on('update', function(updatedTask) { updateEventCount += 1; assert.strictEqual(updatedTask, task); assert.strictEqual(updatedTask.exports, info); }); plan.on('end', function() { assert(progressEventCount >= 3); assert(updateEventCount >= 2); done(); }); plan.start(); }); it("has access to task options, context, and exports", function(done) { var plan = new Plan(); var task = Plan.createTask(SyncTask, "sync", {sonic: "rock", tails: "ice"}); var info = task.exports; plan.addTask(task); plan.on('error', done); var progress = 0; var progressEventCount = 0; plan.on('progress', function(amountDone, amountTotal) { var newProgress = amountDone / amountTotal; assert(newProgress >= progress, "old progress: " + progress + ", new progress: " + newProgress + ", amountDone: " + amountDone + ", amountTotal: " + amountTotal); progressEventCount += 1; progress = newProgress; }); var updateEventCount = 0; plan.on('update', function(updatedTask) { updateEventCount += 1; assert.strictEqual(updatedTask, task); assert.strictEqual(updatedTask.exports, info); }); plan.on('end', function(results) { assert.strictEqual(results.foo, "foo2"); assert.strictEqual(results.tails, "ice"); assert.strictEqual(info.sonic, "rock"); assert.strictEqual(info.amountDone, 1); assert.strictEqual(info.amountTotal, 1); assert.strictEqual(progressEventCount, 1) assert.strictEqual(updateEventCount, 2); done(); }); plan.start({foo: "hi"}); }); it("emits errors", function(done) { var plan = new Plan(); var errorTask = Plan.createTask(ErrorTask); var fastTask = Plan.createTask(FastTask); var smoothTask = Plan.createTask(SmoothTask); plan.addTask(errorTask); plan.addDependency(errorTask, fastTask); plan.addDependency(fastTask, smoothTask); var hadError = false; plan.on('error', function(err) { assert.ok(err.isErrorTask); assert.strictEqual(fastTask.exports.derp, "hi"); assert.strictEqual(fastTask.exports.complete, true); assert.strictEqual(smoothTask.exports.complete, true); hadError = true; }); plan.on('end', function() { assert.ok(hadError); done(); }); plan.start(); }); it("passes context sequentially", function(done) { var plan = new Plan(); var syncTask = Plan.createTask(SyncTask); var fastTask = Plan.createTask(FastTask); var smoothTask = Plan.createTask(SmoothTask); plan.addTask(syncTask); plan.addDependency(syncTask, fastTask); plan.addDependency(fastTask, smoothTask); plan.on('error', done); plan.on('end', function(results) { assert.strictEqual(results.eggman, "no"); done(); }); plan.start({eggman: "no"}); }); it("a task with 2 dependencies", function(done) { var plan = new Plan(); var setTask1 = Plan.createTask(SetNumberTask, "set1", {field: "a", value: 99}); var setTask2 = Plan.createTask(SetNumberTask, "set2", {field: "b", value: 11}); var sumTask = Plan.createTask(SumTask); plan.addTask(sumTask); plan.addDependency(sumTask, setTask1); plan.addDependency(sumTask, setTask2); plan.on('end', function(results) { assert.strictEqual(results.result, 110); done(); }); plan.start(); }); it("runs a task only once if it is depended on twice", function(done) { var plan = new Plan(); var counterTask = Plan.createTask(CounterTask); var fastTask = Plan.createTask(FastTask); var smoothTask = Plan.createTask(SmoothTask); plan.addTask(fastTask); plan.addTask(smoothTask); plan.addDependency(fastTask, counterTask); plan.addDependency(smoothTask, counterTask); plan.on('error', done); plan.on('end', function() { assert.strictEqual(counterTaskCounter, 1); done(); }); plan.start(); }); it("recognizes the ignoreDependencyErrors option, case 1", function(done) { var optsIgnoreErrs = { ignoreDependencyErrors: true }; var plan = new Plan(); var syncTask = Plan.createTask(SyncTask, "sync"); var errorTask2 = Plan.createTask(ErrorTask, "error2"); var fastTask = Plan.createTask(FastTask, "fast", optsIgnoreErrs); var smoothTask = Plan.createTask(SmoothTask, "smooth", optsIgnoreErrs); var errorTask = Plan.createTask(ErrorTask, "error"); plan.addTask(syncTask); plan.addDependency(syncTask, errorTask2); plan.addDependency(errorTask2, fastTask); plan.addDependency(fastTask, smoothTask); plan.addDependency(smoothTask, errorTask); var errorEventCount = 0; plan.on('error', function(err, task) { assert.ok(err.isErrorTask); errorEventCount += 1; if (errorEventCount === 1) { assert.strictEqual(task, errorTask); } else { assert.strictEqual(task, errorTask2); } }); plan.on('end', function(results) { // fast and smooth should have run assert.ok(fastTask.exports.complete); assert.ok(smoothTask.exports.complete); // sync should not have run assert.ok(!syncTask.exports.complete); // error should have been emitted assert.strictEqual(errorEventCount, 2); done(); }); plan.start({eggman: "no"}); }); it("recognizes the ignoreDependencyErrors option, case 2", function(done) { var optsIgnoreErrs = { ignoreDependencyErrors: true }; var plan = new Plan(); var syncTask = Plan.createTask(SyncTask, "sync", optsIgnoreErrs); var fastTask = Plan.createTask(FastTask, "fast"); var smoothTask = Plan.createTask(SmoothTask, "smooth"); var errorTask = Plan.createTask(ErrorTask, "error"); plan.addTask(syncTask); plan.addDependency(syncTask, fastTask); plan.addDependency(fastTask, smoothTask); plan.addDependency(smoothTask, errorTask); var errorEventCount = 0; plan.on('error', function(err, task) { assert.ok(err.isErrorTask); errorEventCount += 1; assert.strictEqual(task, errorTask); }); plan.on('end', function(results) { // fast and smooth should not have run assert.ok(!fastTask.exports.complete); assert.ok(!smoothTask.exports.complete); // sync should have run assert.ok(syncTask.exports.complete); // error should have been emitted assert.strictEqual(errorEventCount, 1); done(); }); plan.start({eggman: "no"}); }); it("has smooth progress on 2nd try with tasks that do not emit progress", function(done) { this.timeout(12000); var plan = createPlan(); plan.on('error', done); var done1000 = false; var done3000 = false; var expectedTimePassed = 4033; var debugOutput = ""; var minDiff = 1, maxDiff = 0, sumDiff = 0; function round(n) { return Math.round(n * 100) / 100; } var firstSumDiff; var firstMaxDiff; plan.on('progress', function(amountDone, amountTotal) { if (! done1000 && plan._task1000.exports.amountDone === 1000) { done1000 = true; debugOutput += "done 1000\n" } if (! done3000 && plan._task3000.exports.amountDone === 3000) { done3000 = true; debugOutput += "done 3000\n" } var timePassed = (new Date()).getTime() - plan._task1000.exports.startDate.getTime(); var expectedPercent = timePassed / expectedTimePassed; var diff = Math.abs(expectedPercent - amountDone); if (diff < minDiff) minDiff = diff; if (diff > maxDiff) maxDiff = diff; sumDiff += diff; debugOutput += "expected " + round(expectedPercent) + " actual " + round(amountDone) + " diff " + round(diff) + "\n"; }); plan.on('end', function() { expectedTimePassed = (new Date()).getTime() - plan._task1000.exports.startDate.getTime(); plan = createPlan(); plan.on('error', done); var done1000 = false; var done3000 = false; var progress = 0; var worstProgressDownAmt = 0; plan.on('progress', function(amountDone) { var newProgress = amountDone; var thisProgressDownAmt = progress - newProgress; if (thisProgressDownAmt > worstProgressDownAmt) worstProgressDownAmt = thisProgressDownAmt progress = newProgress; if (! done1000 && plan._task1000.exports.amountDone === 1000) { done1000 = true; debugOutput += "done 1000\n"; } if (! done3000 && plan._task3000.exports.amountDone === 3000) { done3000 = true; debugOutput += "done 3000\n"; } var timePassed = (new Date()).getTime() - plan._task1000.exports.startDate.getTime(); var expectedPercent = timePassed / expectedTimePassed; var diff = Math.abs(expectedPercent - amountDone); if (diff < minDiff) minDiff = diff; if (diff > maxDiff) maxDiff = diff; sumDiff += diff; debugOutput += "expected " + round(expectedPercent) + " actual " + round(amountDone) + " diff " + round(diff) + "\n"; }); plan.on('end', function() { debugOutput += "min diff " + round(minDiff) + " max diff " + round(maxDiff) + " sum diff " + round(sumDiff) + "\n"; if (worstProgressDownAmt > 0.01) { console.log(debugOutput); throw new Error("2nd time progress went down by more than 0.01"); } if (sumDiff > firstSumDiff) { console.log(debugOutput); throw new Error("2nd time was overall less accurate than first"); } if (maxDiff > firstMaxDiff) { console.log(debugOutput); throw new Error("2nd time worst case progress was worse than first"); } done(); }); firstSumDiff = sumDiff; firstMaxDiff = maxDiff; debugOutput += " min diff " + round(minDiff) + " max diff " + round(maxDiff) + " sum diff " + round(sumDiff) + "\n"; minDiff = 1; maxDiff = 0; sumDiff = 0; debugOutput += "\nnew plan\n"; plan.start(); }); plan.start(); function createPlan() { var plan = new Plan("test-smooth-progress"); var task1000 = Plan.createTask(DelayTask, "delay1000", {timeout: 1000}); var task3000 = Plan.createTask(DelayTask, "delay3000", {timeout: 3000}); var fastTask = Plan.createTask(FastTask, "fastTask"); plan.addTask(fastTask); plan.addDependency(fastTask, task3000); plan.addDependency(task3000, task1000); plan._task1000 = task1000; plan._task3000 = task3000; plan._fastTask = fastTask; return plan; } }); it("limits cpu bound tasks to one per worker count", function(done) { this.timeout(4000); Plan.setWorkerCap(2); var task1 = Plan.createTask(CpuTask); var task2 = Plan.createTask(CpuTask); var task3 = Plan.createTask(CpuTask); var fastTask = Plan.createTask(FastTask); var plan = new Plan(); plan.addTask(fastTask); plan.addTask(task1); plan.addTask(task2); plan.addTask(task3); var success = false; var debugOutput = ""; plan.on('progress', function(amountDone) { var cpuTaskProcessingCount = 0; if (task1.exports.state === 'processing') cpuTaskProcessingCount += 1; if (task2.exports.state === 'processing') cpuTaskProcessingCount += 1; if (task3.exports.state === 'processing') cpuTaskProcessingCount += 1; if (cpuTaskProcessingCount === 2 && fastTask.exports.state === 'complete' && (task1.exports.state === 'queued' || task2.exports.state === 'queued' || task3.exports.state === 'queued')) { success = true; } else { debugOutput += "fastTask " + fastTask.exports.state + " task1 " + task1.exports.state + " task2 " + task2.exports.state + " task3 " + task3.exports.state + "\n"; } }); plan.on('error', done); plan.on('end', function() { if (! success) { console.log(debugOutput); throw new Error("failed to limit cpu bound tasks"); } done(); }); plan.start(); }); });