├── .gitignore ├── .travis.yml ├── buster.js ├── Readme.md ├── lib ├── on-interrupt.js └── buster-autotest.js ├── package.json └── test └── buster-autotest-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.6 4 | -------------------------------------------------------------------------------- /buster.js: -------------------------------------------------------------------------------- 1 | exports.tests = { 2 | environment: "node", 3 | tests: ["test/**/*.js"] 4 | }; 5 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # buster-autotest 2 | 3 | [![Build status](https://secure.travis-ci.org/busterjs/buster-autotest.png?branch=master)](http://travis-ci.org/busterjs/buster-autotest) 4 | 5 | A simple autotest CLI for Buster.JS 6 | -------------------------------------------------------------------------------- /lib/on-interrupt.js: -------------------------------------------------------------------------------- 1 | module.exports = function (message, callback) { 2 | var interrupted = false; 3 | process.on('SIGINT', function () { 4 | if (interrupted) { 5 | process.exit(); 6 | } 7 | console.log(message + " Interrupt a second time to quit"); 8 | interrupted = true; 9 | setTimeout(function () { 10 | interrupted = false; 11 | callback(); 12 | }, 1500); 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "buster-autotest", 3 | "version": "0.1.1", 4 | "description": "Watch files and run buster tests on save", 5 | "homepage": "http://busterjs.org/docs/autotest", 6 | "author": { 7 | "name": "Christian Johansen", 8 | "email": "christian@cjohansen.no", 9 | "url": "http://cjohansen.no" 10 | }, 11 | "contributors": [{ 12 | "name": "August Lilleaas", 13 | "email": "august.lilleaas@gmail.com", 14 | "url": "http://augustl.com" 15 | }], 16 | "main": "./lib/buster-autotest", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/busterjs/buster-autotest.git" 20 | }, 21 | "scripts": { 22 | "test": "./node_modules/buster/bin/buster-test" 23 | }, 24 | "dependencies": { 25 | "fs-watch-tree": ">=0.1", 26 | "buster-glob": ">=0.3.1" 27 | }, 28 | "devDependencies": { 29 | "buster": "" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/buster-autotest.js: -------------------------------------------------------------------------------- 1 | var wt = require("fs-watch-tree"); 2 | var cp = require("child_process"); 3 | var util = require("util"); 4 | var glob = require("buster-glob"); 5 | var path = require("path"); 6 | var onInterrupt = require("./on-interrupt"); 7 | 8 | function throttle(ms, callback, thisp) { 9 | var timer; 10 | return function () { 11 | var args = arguments; 12 | clearTimeout(timer); 13 | timer = setTimeout(function () { callback.apply(thisp, args); }, ms); 14 | }; 15 | } 16 | 17 | function addTestOption(argv, files) { 18 | for (var i = 0, l = argv.length; i < l; ++i) { 19 | if (argv[i] === "-t" || argv[i] === "--tests") { 20 | var arguments = argv.slice(); 21 | arguments[i + 1] += "," + files.join(","); 22 | return arguments; 23 | } 24 | } 25 | return argv.concat(["-t", files.join(",")]); 26 | } 27 | 28 | var clearScreen = "\x1b[1;1H\x1b[2J"; 29 | 30 | function printHeader() { 31 | var now = new Date(); 32 | var time = now.getHours() + ":" + now.getMinutes() + ":" + now.getSeconds(); 33 | util.puts(clearScreen + time + " Running tests"); 34 | } 35 | 36 | function success(code) { 37 | return code === 0; 38 | } 39 | 40 | exports.watch = function (dir, options) { 41 | var running = false; 42 | options = options || {}; 43 | 44 | function findFiles(event, callback) { 45 | if (!event || !event.name || event.isDirectory()) { 46 | return callback([]); 47 | } 48 | var pieces = path.basename(event.name).split("."); 49 | pieces.pop(); 50 | var base = pieces.join("."); 51 | glob.glob([ 52 | "{test{,s},spec{,s}}/**/*" + base + "*.js" 53 | ], function (err, paths) { 54 | paths.push(event.name); 55 | callback(paths); 56 | }); 57 | } 58 | 59 | function prepareOptions(files) { 60 | var argv = options.argv || []; 61 | if (files.length > 0) { argv = addTestOption(argv, files); } 62 | return argv; 63 | } 64 | 65 | function runTests(event) { 66 | if (running) { return; } 67 | running = true; 68 | findFiles(event, function (files) { 69 | printHeader(); 70 | var test = cp.spawn("buster-test", prepareOptions(files)); 71 | 72 | var cancel = throttle(1000, function () { 73 | running = false; 74 | test.kill(); 75 | }); 76 | 77 | function onData(data) { 78 | util.print(data.toString()); 79 | cancel(); 80 | } 81 | 82 | test.stdout.on("data", onData); 83 | test.stderr.on("data", onData); 84 | 85 | test.on("exit", function (code) { 86 | running = false; 87 | if (success(code) && files.length > 0) { runTests(); } 88 | }); 89 | }); 90 | } 91 | 92 | var run = throttle(10, runTests); 93 | 94 | wt.watchTree(dir, { exclude: [/\/\./, "#", "node_modules"] }, function (e) { 95 | if (e.isMkdir()) { return; } 96 | run(e); 97 | }); 98 | 99 | onInterrupt("Running all tests.", run); 100 | }; 101 | -------------------------------------------------------------------------------- /test/buster-autotest-test.js: -------------------------------------------------------------------------------- 1 | var buster = require("buster"); 2 | var autotest = require("../lib/buster-autotest"); 3 | var wt = require("fs-watch-tree"); 4 | var cp = require("child_process"); 5 | var util = require("util"); 6 | var path = require("path"); 7 | var glob = require("buster-glob"); 8 | 9 | buster.testCase("Autotest", { 10 | setUp: function () { 11 | var self = this; 12 | this.stdout = ""; 13 | this.stub(util, "print", function (str) { self.stdout += str; }); 14 | this.stub(util, "puts", function (str) { self.stdout += str + "\n"; }); 15 | this.processes = []; 16 | this.stub(cp, "spawn", function () { 17 | var process = buster.eventEmitter.create(); 18 | process.kill = self.spy(); 19 | process.stdout = buster.eventEmitter.create(); 20 | process.stderr = buster.eventEmitter.create(); 21 | self.processes.push(process); 22 | self.process = process; 23 | return process; 24 | }); 25 | this.stub(wt, "watchTree"); 26 | this.stub(glob, "glob").yields(null, []); 27 | this.clock = this.useFakeTimers(); 28 | }, 29 | 30 | "watches directory": function () { 31 | autotest.watch("/some/dir"); 32 | assert.calledOnceWith(wt.watchTree, "/some/dir"); 33 | }, 34 | 35 | "on change": { 36 | setUp: function () { 37 | this.emitChange = function (name, opt) { 38 | opt = opt || {}; 39 | wt.watchTree.args[0][2]({ 40 | isMkdir: function () { return opt.isMkdir; }, 41 | isDirectory: function () { return opt.isDir; }, 42 | name: name 43 | }); 44 | }; 45 | this.failTests = function () { this.process.emit("exit", 1); }; 46 | this.passTests = function () { this.process.emit("exit", 0); }; 47 | }, 48 | 49 | "does not run tests immediately": function () { 50 | autotest.watch("/some/dir"); 51 | this.emitChange(); 52 | refute.called(cp.spawn); 53 | }, 54 | 55 | "does not run tests for mkdir event": function () { 56 | autotest.watch("/some/dir"); 57 | this.emitChange("test", { isMkdir: true }); 58 | this.clock.tick(10); 59 | refute.called(cp.spawn); 60 | }, 61 | 62 | "runs tests after 10ms": function () { 63 | autotest.watch("/some/dir"); 64 | this.emitChange(); 65 | this.clock.tick(10); 66 | assert.calledOnce(cp.spawn); 67 | assert.calledWith(cp.spawn, "buster-test"); 68 | }, 69 | 70 | "runs specific test": function () { 71 | autotest.watch("/some/dir"); 72 | this.emitChange("some/file.js"); 73 | this.clock.tick(10); 74 | assert.calledWith(cp.spawn, "buster-test", ["-t", "some/file.js"]); 75 | }, 76 | 77 | "runs all tests when name is unavailable": function () { 78 | autotest.watch("/some/dir"); 79 | this.emitChange(); 80 | this.clock.tick(10); 81 | assert.calledWith(cp.spawn, "buster-test", []); 82 | }, 83 | 84 | "runs all tests when directory": function () { 85 | autotest.watch("/some/dir"); 86 | this.emitChange("test", { isDir: true }); 87 | this.clock.tick(10); 88 | assert.calledWith(cp.spawn, "buster-test", []); 89 | }, 90 | 91 | "adds changed test file to existing -t argument": function () { 92 | autotest.watch("/some/dir", { argv: ["-t", "file.js"] }); 93 | this.emitChange("test/thing-test.js"); 94 | this.clock.tick(10); 95 | assert.calledWith( 96 | cp.spawn, "buster-test", ["-t", "file.js,test/thing-test.js"] 97 | ); 98 | }, 99 | 100 | "uses --tests if present": function () { 101 | autotest.watch("/some/dir", { argv: ["--tests", "file.js"] }); 102 | this.emitChange("test/thing-test.js"); 103 | this.clock.tick(10); 104 | assert.calledWith( 105 | cp.spawn, "buster-test", ["--tests", "file.js,test/thing-test.js"] 106 | ); 107 | }, 108 | 109 | "runs with all provided options": function () { 110 | autotest.watch("/some/dir", { 111 | argv: ["-r", "specification", "--tests", "file.js", "--node"] 112 | }); 113 | this.emitChange("test/thing-test.js"); 114 | this.clock.tick(10); 115 | assert.calledWith( 116 | cp.spawn, "buster-test", [ 117 | "-r", 118 | "specification", 119 | "--tests", 120 | "file.js,test/thing-test.js", 121 | "--node" 122 | ] 123 | ); 124 | }, 125 | 126 | "does not run if already running": function () { 127 | autotest.watch("/some/dir", { argv: ["--tests", "file.js"] }); 128 | this.emitChange("test/thing-test.js"); 129 | this.clock.tick(10); 130 | this.emitChange("test/thing-test.js"); 131 | this.clock.tick(10); 132 | 133 | assert.calledOnce(cp.spawn); 134 | }, 135 | 136 | "issues new run when previous is completed": function () { 137 | autotest.watch("/some/dir", { argv: ["--tests", "file.js"] }); 138 | this.emitChange("test/thing-test.js"); 139 | this.clock.tick(10); 140 | this.process.emit("exit"); 141 | this.emitChange("test/thing-test.js"); 142 | this.clock.tick(10); 143 | 144 | assert.calledTwice(cp.spawn); 145 | }, 146 | 147 | "kills test process after 1 second": function () { 148 | autotest.watch("/some/dir", { argv: ["--tests", "file.js"] }); 149 | this.emitChange("test/thing-test.js"); 150 | this.clock.tick(10); 151 | this.process.stdout.emit("data", "Running..."); 152 | this.clock.tick(1000); 153 | 154 | assert.calledOnce(this.process.kill); 155 | }, 156 | 157 | "runs tests after killing process": function () { 158 | autotest.watch("/some/dir", { argv: ["--tests", "file.js"] }); 159 | this.emitChange("test/thing-test.js"); 160 | this.clock.tick(10); 161 | this.process.stdout.emit("data", "Running..."); 162 | this.clock.tick(1000); 163 | this.emitChange("test/thing-test.js"); 164 | this.clock.tick(10); 165 | 166 | assert.calledTwice(cp.spawn); 167 | }, 168 | 169 | "runs all tests when passing after failing": function () { 170 | autotest.watch("/some/dir"); 171 | this.emitChange("test/thing-test.js"); 172 | this.clock.tick(10); 173 | this.failTests(); 174 | this.emitChange("test/thing-test.js"); 175 | this.clock.tick(10); 176 | this.passTests(); 177 | 178 | assert.calledThrice(cp.spawn); 179 | assert.calledWith(cp.spawn, "buster-test", []); 180 | }, 181 | 182 | "runs originally selected tests when passing after failing": function () { 183 | autotest.watch("/some/dir", { argv: ["-t", "test/boing.js"] }); 184 | this.emitChange("test/thing-test.js"); 185 | this.clock.tick(10); 186 | this.failTests(); 187 | this.emitChange("test/thing-test.js"); 188 | this.clock.tick(10); 189 | this.passTests(); 190 | 191 | assert.calledThrice(cp.spawn); 192 | assert.calledWith(cp.spawn, "buster-test", ["-t", "test/boing.js"]); 193 | }, 194 | 195 | "does not run all tests when failing after passing": function () { 196 | autotest.watch("/some/dir"); 197 | this.emitChange("test/thing-test.js"); 198 | this.clock.tick(10); 199 | this.passTests(); 200 | this.emitChange("test/thing-test.js"); 201 | this.clock.tick(10); 202 | this.failTests(); 203 | 204 | assert.calledTwice(cp.spawn); 205 | }, 206 | 207 | "does not re-run all tests after all passing": function () { 208 | autotest.watch("/some/dir"); 209 | this.emitChange("test/thing-test.js"); 210 | this.clock.tick(10); 211 | this.passTests(); 212 | this.clock.tick(20); 213 | 214 | assert.calledTwice(cp.spawn); 215 | }, 216 | 217 | "runs related test files": function () { 218 | glob.glob.yields(null, ["test/buster-autotest-test.js"]); 219 | autotest.watch(path.join(__dirname, "../")); 220 | this.emitChange("lib/buster-autotest.js"); 221 | this.clock.tick(10); 222 | 223 | assert.calledWith(cp.spawn, "buster-test", [ 224 | "-t", 225 | "test/buster-autotest-test.js,lib/buster-autotest.js" 226 | ]); 227 | } 228 | } 229 | }); 230 | --------------------------------------------------------------------------------