globwatcher
Version:
watch a set of files for changes (including create/delete) by glob patterns
466 lines (430 loc) • 18 kB
JavaScript
;
var fs = require("fs");
var globwatcher = require("../../lib/globwatcher/globwatcher");
var minimatch = require("minimatch");
var mocha_sprinkles = require("mocha-sprinkles");
var path = require("path");
var Promise = require("bluebird");
var shell = require("shelljs");
var should = require("should");
var touch = require("touch");
var util = require("util");
var future = mocha_sprinkles.future;
var withTempFolder = mocha_sprinkles.withTempFolder;
require("source-map-support").install();
function makeFixtures(folder, ts) {
if (!ts) ts = Date.now() - 1000;
["" + folder + "/one.x", "" + folder + "/sub/one.x", "" + folder + "/sub/two.x", "" + folder + "/nested/three.x", "" + folder + "/nested/weird.jpg"].map(function (file) {
shell.mkdir("-p", path.dirname(file));
touch.sync(file, { mtime: ts });
});
}
function fixtures(f) {
return future(withTempFolder(function (folder) {
makeFixtures(folder);
return f(folder);
}));
}
// create a new globwatch, run a test, and close it
function withGlobwatcher(pattern, options, f) {
if (f == null) {
f = options;
options = {};
}
options.persistent = false;
var g = globwatcher.globwatcher(pattern, options);
return g.ready["finally"](function () {
return f(g);
})["finally"](function () {
return g.close();
});
}
// capture add/remove/change into an object for later inspection
function capture(g) {
var summary = {};
function push(name, item) {
if (summary[name] == null) summary[name] = [];
summary[name].push(item);
summary[name].sort();
}
g.on("added", function (filename) {
return push("added", filename);
});
g.on("deleted", function (filename) {
return push("deleted", filename);
});
g.on("changed", function (filename) {
return push("changed", filename);
});
return summary;
}
describe("globwatcher", function () {
it("folderMatchesMinimatchPrefix", function () {
var set = new minimatch.Minimatch("/home/commie/**/*.js", { nonegate: true }).set[0];
globwatcher.folderMatchesMinimatchPrefix(["", "home"], set).should.equal(true);
globwatcher.folderMatchesMinimatchPrefix(["", "home", "commie"], set).should.equal(true);
globwatcher.folderMatchesMinimatchPrefix(["", "home", "robey"], set).should.equal(false);
globwatcher.folderMatchesMinimatchPrefix(["", "home", "commie", "rus"], set).should.equal(true);
set = new minimatch.Minimatch("/home/commie/l*/*.js", { nonegate: true }).set[0];
globwatcher.folderMatchesMinimatchPrefix(["", "home"], set).should.equal(true);
globwatcher.folderMatchesMinimatchPrefix(["", "home", "commie"], set).should.equal(true);
globwatcher.folderMatchesMinimatchPrefix(["", "home", "robey"], set).should.equal(false);
globwatcher.folderMatchesMinimatchPrefix(["", "home", "commie", "rus"], set).should.equal(false);
globwatcher.folderMatchesMinimatchPrefix(["", "home", "commie", "lola"], set).should.equal(true);
globwatcher.folderMatchesMinimatchPrefix(["", "home", "commie", "lola", "prissy"], set).should.equal(false);
});
it("addWatch", future(function () {
return withGlobwatcher("/~~nonexistent~~", function (g) {
["/absolute.txt", "/sub/absolute.txt", "/deeply/nested/file/why/nobody/knows.txt"].forEach(function (f) {
return g.addWatch(f);
});
g.watchMap.getFolders().sort().should.eql(["/", "/deeply/nested/file/why/nobody/", "/sub/"]);
g.watchMap.getFilenames("/").should.eql(["/absolute.txt"]);
g.watchMap.getFilenames("/sub/").should.eql(["/sub/absolute.txt"]);
g.watchMap.getFilenames("/deeply/nested/file/why/nobody/").should.eql(["/deeply/nested/file/why/nobody/knows.txt"]);
});
}));
it("can parse patterns", fixtures(function (folder) {
return withGlobwatcher("" + folder + "/**/*.x", function (g) {
g.patterns.should.eql(["" + folder + "/**/*.x"]);
Object.keys(g.watchers).sort().should.eql(["" + folder + "/", "" + folder + "/nested/", "" + folder + "/sub/"]);
g.watchMap.getFolders().sort().should.eql(["" + folder + "/", "" + folder + "/nested/", "" + folder + "/sub/"]);
g.watchMap.getFilenames("" + folder + "/").should.eql(["" + folder + "/one.x"]);
g.watchMap.getFilenames("" + folder + "/nested/").should.eql(["" + folder + "/nested/three.x"]);
g.watchMap.getFilenames("" + folder + "/sub/").should.eql(["" + folder + "/sub/one.x", "" + folder + "/sub/two.x"]);
});
}));
it("can parse patterns relative to cwd", fixtures(function (folder) {
return withGlobwatcher("**/*.x", { cwd: "" + folder + "/sub" }, function (g) {
g.patterns.should.eql(["" + folder + "/sub/**/*.x"]);
Object.keys(g.watchers).sort().should.eql(["" + folder + "/sub/"]);
g.watchMap.getFolders().sort().should.eql(["" + folder + "/sub/"]);
g.watchMap.getFilenames("" + folder + "/sub/").should.eql(["" + folder + "/sub/one.x", "" + folder + "/sub/two.x"]);
});
}));
it("handles odd relative paths", fixtures(function (folder) {
return withGlobwatcher("../sub/**/*.x", { cwd: "" + folder + "/nested" }, function (g) {
Object.keys(g.watchers).sort().should.eql(["" + folder + "/sub/"]);
g.watchMap.getFolders().sort().should.eql(["" + folder + "/sub/"]);
g.watchMap.getFilenames("" + folder + "/sub/").should.eql(["" + folder + "/sub/one.x", "" + folder + "/sub/two.x"]);
});
}));
it("notices new files", fixtures(function (folder) {
var summary = null;
return withGlobwatcher("" + folder + "/**/*.x", function (g) {
summary = capture(g);
touch.sync("" + folder + "/nested/four.x");
touch.sync("" + folder + "/sub/not-me.txt");
return g.check().then(function () {
summary.should.eql({
added: ["" + folder + "/nested/four.x"]
});
});
});
}));
it("doesn't emit signals when turned off", fixtures(function (folder) {
var summary = null;
return withGlobwatcher("" + folder + "/**/*.x", function (g) {
summary = capture(g);
g.stopWatches();
return Promise.delay(50).then(function () {
touch.sync("" + folder + "/nested/four.x");
touch.sync("" + folder + "/sub/not-me.txt");
g.check();
}).then(function () {
// just in case, make sure the timer goes off too
return Promise.delay(g.interval * 2);
}).then(function () {
summary.should.eql({});
});
});
}));
it("notices new files only in cwd", fixtures(function (folder) {
var summary = null;
return withGlobwatcher("**/*.x", { cwd: "" + folder + "/sub" }, function (g) {
summary = capture(g);
touch.sync("" + folder + "/nested/four.x");
touch.sync("" + folder + "/sub/not-me.txt");
touch.sync("" + folder + "/sub/four.x");
return g.check().then(function () {
summary.should.eql({
added: ["" + folder + "/sub/four.x"]
});
});
});
}));
it("notices new files nested deeply", fixtures(function (folder) {
var summary = null;
return withGlobwatcher("" + folder + "/**/*.x", function (g) {
summary = capture(g);
shell.mkdir("-p", "" + folder + "/nested/more/deeply");
touch.sync("" + folder + "/nested/more/deeply/nine.x");
return g.check().then(function () {
summary.should.eql({
added: ["" + folder + "/nested/more/deeply/nine.x"]
});
});
});
}));
it("notices deleted files", fixtures(function (folder) {
var summary = null;
return withGlobwatcher("**/*.x", { cwd: "" + folder }, function (g) {
summary = capture(g);
fs.unlinkSync("" + folder + "/sub/one.x");
return g.check().then(function () {
summary.should.eql({
deleted: ["" + folder + "/sub/one.x"]
});
});
});
}));
it("notices a rename as an add + delete", fixtures(function (folder) {
var summary = null;
return withGlobwatcher("**/*.x", { cwd: "" + folder }, function (g) {
summary = capture(g);
fs.renameSync("" + folder + "/sub/two.x", "" + folder + "/sub/twelve.x");
return g.check().then(function () {
summary.should.eql({
added: ["" + folder + "/sub/twelve.x"],
deleted: ["" + folder + "/sub/two.x"]
});
});
});
}));
it("handles a nested delete", fixtures(function (folder) {
shell.mkdir("-p", "" + folder + "/nested/more/deeply");
touch.sync("" + folder + "/nested/more/deeply/here.x");
var summary = null;
return withGlobwatcher("**/*.x", { cwd: "" + folder }, function (g) {
summary = capture(g);
shell.rm("-r", "" + folder + "/nested");
return g.check().then(function () {
summary.should.eql({
deleted: ["" + folder + "/nested/more/deeply/here.x", "" + folder + "/nested/three.x"]
});
});
});
}));
it("handles a changed file", fixtures(function (folder) {
var summary = null;
return withGlobwatcher("**/*.x", { cwd: "" + folder }, function (g) {
summary = capture(g);
fs.writeFileSync("" + folder + "/sub/one.x", "gahhhhhh");
return g.check().then(function () {
summary.should.eql({
changed: ["" + folder + "/sub/one.x"]
});
});
});
}));
it("follows a safe-write", fixtures(function (folder) {
var summary = null;
var savee = "" + folder + "/one.x";
var backup = "" + folder + "/one.x~";
return withGlobwatcher("**/*.x", { cwd: "" + folder }, function (g) {
summary = capture(g);
fs.writeFileSync(backup, fs.readFileSync(savee));
fs.unlinkSync(savee);
fs.renameSync(backup, savee);
return g.check().then(function () {
summary.should.eql({
changed: [savee]
});
});
});
}));
it("only emits once for a changed file", fixtures(function (folder) {
var summary = null;
return withGlobwatcher("**/*.x", { cwd: "" + folder }, function (g) {
summary = capture(g);
fs.writeFileSync("" + folder + "/one.x", "whee1");
return g.check().then(function () {
summary.should.eql({
changed: ["" + folder + "/one.x"]
});
return g.check();
}).then(function () {
summary.should.eql({
changed: ["" + folder + "/one.x"]
});
});
});
}));
it("emits twice if a file was changed twice", fixtures(function (folder) {
var summary = null;
return withGlobwatcher("**/*.x", { cwd: "" + folder }, function (g) {
summary = capture(g);
fs.writeFileSync("" + folder + "/one.x", "whee1");
return g.check().then(function () {
summary.should.eql({
changed: ["" + folder + "/one.x"]
});
fs.writeFileSync("" + folder + "/one.x", "whee123");
return g.check();
}).then(function () {
summary.should.eql({
changed: ["" + folder + "/one.x", "" + folder + "/one.x"]
});
});
});
}));
it("doesn't mind watching a nonexistent folder", fixtures(function (folder) {
return withGlobwatcher("" + folder + "/not/there/*", function (g) {
3..should.equal(3);
});
}));
it("sees a new matching file even if the whole folder was missing when it started", future(withTempFolder(function (folder) {
var summary = null;
return withGlobwatcher("" + folder + "/not/there/*", function (g) {
summary = capture(g);
shell.mkdir("-p", "" + folder + "/not/there");
fs.writeFileSync("" + folder + "/not/there/ten.x", "wheeeeeee");
return g.check().then(function () {
summary.should.eql({
added: ["" + folder + "/not/there/ten.x"]
});
});
});
})));
it("sees a new matching file even if nested folders were missing when it started", fixtures(function (folder) {
var summary = null;
return withGlobwatcher("" + folder + "/sub/deeper/*.x", function (g) {
summary = capture(g);
shell.mkdir("-p", "" + folder + "/sub/deeper");
fs.writeFileSync("" + folder + "/sub/deeper/ten.x", "wheeeeeee");
return g.check().then(function () {
summary.should.eql({
added: ["" + folder + "/sub/deeper/ten.x"]
});
});
});
}));
it("sees a new matching file even if the entire tree was erased and re-created", fixtures(function (folder) {
shell.rm("-rf", "" + folder + "/nested");
shell.mkdir("-p", "" + folder + "/nested/deeper/still");
touch.sync("" + folder + "/nested/deeper/still/four.x");
var summary = null;
return withGlobwatcher("" + folder + "/**/*", function (g) {
summary = capture(g);
shell.rm("-r", "" + folder + "/nested");
return g.check().then(function () {
summary.should.eql({
deleted: ["" + folder + "/nested/deeper/still/four.x"]
});
delete summary.deleted;
shell.mkdir("-p", "" + folder + "/nested/deeper/still");
fs.writeFileSync("" + folder + "/nested/deeper/still/ten.x", "wheeeeeee");
return g.check();
}).then(function () {
summary.should.eql({
added: ["" + folder + "/nested/deeper/still/ten.x"]
});
});
});
}));
it("sees a new matching file even if the folder exists but was empty", fixtures(function (folder) {
shell.mkdir("-p", "" + folder + "/nested/deeper");
var summary = null;
return withGlobwatcher("" + folder + "/nested/deeper/*.x", function (g) {
summary = capture(g);
fs.writeFileSync("" + folder + "/nested/deeper/ten.x", "wheeeeeee");
return g.check().then(function () {
summary.should.eql({
added: ["" + folder + "/nested/deeper/ten.x"]
});
});
});
}));
it("emits signals for folders when asked", fixtures(function (folder) {
var summary = null;
return withGlobwatcher("" + folder + "/**/*", { emitFolders: true }, function (g) {
summary = capture(g);
shell.mkdir("-p", "" + folder + "/newfolder");
shell.rm("-r", "" + folder + "/nested");
return g.check().then(function () {
summary.should.eql({
added: ["" + folder + "/newfolder/"],
deleted: ["" + folder + "/nested/", "" + folder + "/nested/three.x", "" + folder + "/nested/weird.jpg"]
});
});
});
}));
it("will watch a single, non-globbed file that doesn't exist", fixtures(function (folder) {
var summary = null;
return withGlobwatcher("" + folder + "/nothing.x", function (g) {
summary = capture(g);
fs.writeFileSync("" + folder + "/nothing.x", "hi!");
return Promise.delay(g.interval).then(function () {
summary.should.eql({
added: ["" + folder + "/nothing.x"]
});
});
});
}));
it("returns a currentSet", fixtures(function (folder) {
return withGlobwatcher("" + folder + "/**/*.x", function (g) {
g.currentSet().sort().should.eql(["" + folder + "/nested/three.x", "" + folder + "/one.x", "" + folder + "/sub/one.x", "" + folder + "/sub/two.x"]);
shell.rm("" + folder + "/sub/one.x");
fs.writeFileSync("" + folder + "/whatevs.x");
return g.check().then(function () {
g.currentSet().sort().should.eql(["" + folder + "/nested/three.x", "" + folder + "/one.x", "" + folder + "/sub/two.x", "" + folder + "/whatevs.x"]);
});
});
}));
describe("takes a snapshot", function () {
it("of globs", fixtures(function (folder) {
return withGlobwatcher("" + folder + "/**/*.x", { snapshot: {} }, function (g) {
var ts = fs.statSync("" + folder + "/one.x").mtime.getTime();
fs.writeFileSync("" + folder + "/wut.x", "hello");
touch.sync("" + folder + "/wut.x", { mtime: ts });
return g.check().then(function () {
var snapshot = g.snapshot();
snapshot["" + folder + "/one.x"].should.eql({ mtime: ts, size: 0 });
snapshot["" + folder + "/wut.x"].should.eql({ mtime: ts, size: 5 });
snapshot["" + folder + "/nested/three.x"].should.eql({ mtime: ts, size: 0 });
snapshot["" + folder + "/sub/one.x"].should.eql({ mtime: ts, size: 0 });
snapshot["" + folder + "/sub/two.x"].should.eql({ mtime: ts, size: 0 });
});
});
}));
it("of normal files", fixtures(function (folder) {
return withGlobwatcher("" + folder + "/sub/two.x", { snapshot: {} }, function (g) {
var ts = fs.statSync("" + folder + "/sub/two.x").mtime.getTime();
return g.check().then(function () {
var snapshot = g.snapshot();
snapshot["" + folder + "/sub/two.x"].should.eql({ mtime: ts, size: 0 });
fs.writeFileSync("" + folder + "/sub/two.x", "new!");
ts = fs.statSync("" + folder + "/sub/two.x").mtime.getTime();
return withGlobwatcher("" + folder + "/sub/two.x", { snapshot: snapshot }, function (g) {
return g.check().then(function () {
snapshot = g.snapshot();
snapshot["" + folder + "/sub/two.x"].should.eql({ mtime: ts, size: 4 });
});
});
});
});
}));
});
it("resumes from a snapshot", fixtures(function (folder) {
return withGlobwatcher("" + folder + "/**/*.x", function (g) {
var summary = null;
var snapshot = g.snapshot();
g.close();
return Promise.delay(100).then(function () {
fs.writeFileSync("" + folder + "/one.x", "hello");
shell.rm("" + folder + "/sub/two.x");
touch.sync("" + folder + "/sub/nine.x");
g = globwatcher.globwatcher("" + folder + "/**/*.x", { persistent: false, snapshot: snapshot });
summary = capture(g);
return g.ready;
}).then(function () {
summary.should.eql({
added: ["" + folder + "/sub/nine.x"],
changed: ["" + folder + "/one.x"],
deleted: ["" + folder + "/sub/two.x"]
});
});
});
}));
});
//# sourceMappingURL=test_globwatcher.js.map