globwatcher
Version:
watch a set of files for changes (including create/delete) by glob patterns
472 lines (436 loc) • 16.8 kB
JavaScript
let fs = require("fs");
let globwatcher = require("../../lib/globwatcher/globwatcher");
let minimatch = require("minimatch");
let mocha_sprinkles = require("mocha-sprinkles");
let path = require("path");
let Promise = require("bluebird");
let shell = require("shelljs");
let should = require("should");
let touch = require("touch");
let util = require("util");
let future = mocha_sprinkles.future;
let 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((file) => {
shell.mkdir("-p", path.dirname(file));
touch.sync(file, { mtime: ts });
});
}
function fixtures(f) {
return future(withTempFolder((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;
let g = globwatcher.globwatcher(pattern, options);
return g.ready.finally(() => f(g)).finally(() => g.close());
}
// capture add/remove/change into an object for later inspection
function capture(g) {
let summary = {};
function push(name, item) {
if (summary[name] == null) summary[name] = [];
summary[name].push(item);
summary[name].sort();
}
g.on("added", (filename) => push("added", filename));
g.on("deleted", (filename) => push("deleted", filename));
g.on("changed", (filename) => push("changed", filename));
return summary;
}
describe("globwatcher", () => {
it("folderMatchesMinimatchPrefix", () => {
let 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(() => {
return withGlobwatcher("/~~nonexistent~~", (g) => {
[
"/absolute.txt",
"/sub/absolute.txt",
"/deeply/nested/file/why/nobody/knows.txt"
].forEach((f) => 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((folder) => {
return withGlobwatcher(`${folder}/**/*.x`, (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((folder) => {
return withGlobwatcher("**/*.x", { cwd: `${folder}/sub` }, (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((folder) => {
return withGlobwatcher("../sub/**/*.x", { cwd: `${folder}/nested` }, (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((folder) => {
let summary = null;
return withGlobwatcher(`${folder}/**/*.x`, (g) => {
summary = capture(g);
touch.sync(`${folder}/nested/four.x`);
touch.sync(`${folder}/sub/not-me.txt`);
return g.check().then(() => {
summary.should.eql({
added: [ `${folder}/nested/four.x` ]
});
});
});
}));
it("doesn't emit signals when turned off", fixtures((folder) => {
let summary = null;
return withGlobwatcher(`${folder}/**/*.x`, (g) => {
summary = capture(g);
g.stopWatches();
return Promise.delay(50).then(() => {
touch.sync(`${folder}/nested/four.x`);
touch.sync(`${folder}/sub/not-me.txt`);
g.check();
}).then(() => {
// just in case, make sure the timer goes off too
return Promise.delay(g.interval * 2);
}).then(() => {
summary.should.eql({});
});
});
}));
it("notices new files only in cwd", fixtures((folder) => {
let summary = null;
return withGlobwatcher("**/*.x", { cwd: `${folder}/sub` }, (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(() => {
summary.should.eql({
added: [ `${folder}/sub/four.x` ]
});
});
});
}));
it("notices new files nested deeply", fixtures((folder) => {
let summary = null;
return withGlobwatcher(`${folder}/**/*.x`, (g) => {
summary = capture(g);
shell.mkdir("-p", `${folder}/nested/more/deeply`);
touch.sync(`${folder}/nested/more/deeply/nine.x`);
return g.check().then(() => {
summary.should.eql({
added: [ `${folder}/nested/more/deeply/nine.x` ]
});
});
});
}));
it("notices deleted files", fixtures((folder) => {
let summary = null;
return withGlobwatcher("**/*.x", { cwd: `${folder}` }, (g) => {
summary = capture(g);
fs.unlinkSync(`${folder}/sub/one.x`);
return g.check().then(() => {
summary.should.eql({
deleted: [ `${folder}/sub/one.x` ]
});
});
});
}));
it("notices a rename as an add + delete", fixtures((folder) => {
let summary = null;
return withGlobwatcher("**/*.x", { cwd: `${folder}` }, (g) => {
summary = capture(g);
fs.renameSync(`${folder}/sub/two.x`, `${folder}/sub/twelve.x`);
return g.check().then(() => {
summary.should.eql({
added: [ `${folder}/sub/twelve.x` ],
deleted: [ `${folder}/sub/two.x` ]
});
});
});
}));
it("handles a nested delete", fixtures((folder) => {
shell.mkdir("-p", `${folder}/nested/more/deeply`);
touch.sync(`${folder}/nested/more/deeply/here.x`);
let summary = null;
return withGlobwatcher("**/*.x", { cwd: `${folder}` }, (g) => {
summary = capture(g);
shell.rm("-r", `${folder}/nested`);
return g.check().then(() => {
summary.should.eql({
deleted: [ `${folder}/nested/more/deeply/here.x`, `${folder}/nested/three.x` ]
});
});
});
}));
it("handles a changed file", fixtures((folder) => {
let summary = null;
return withGlobwatcher("**/*.x", { cwd: `${folder}` }, (g) => {
summary = capture(g);
fs.writeFileSync(`${folder}/sub/one.x`, "gahhhhhh");
return g.check().then(() => {
summary.should.eql({
changed: [ `${folder}/sub/one.x` ]
});
});
});
}));
it("follows a safe-write", fixtures((folder) => {
let summary = null;
let savee = `${folder}/one.x`;
let backup = `${folder}/one.x~`;
return withGlobwatcher("**/*.x", { cwd: `${folder}` }, (g) => {
summary = capture(g);
fs.writeFileSync(backup, fs.readFileSync(savee));
fs.unlinkSync(savee);
fs.renameSync(backup, savee);
return g.check().then(() => {
summary.should.eql({
changed: [ savee ]
});
});
});
}));
it("only emits once for a changed file", fixtures((folder) => {
let summary = null;
return withGlobwatcher("**/*.x", { cwd: `${folder}` }, (g) => {
summary = capture(g);
fs.writeFileSync(`${folder}/one.x`, "whee1");
return g.check().then(() => {
summary.should.eql({
changed: [ `${folder}/one.x` ]
});
return g.check();
}).then(() => {
summary.should.eql({
changed: [ `${folder}/one.x` ]
});
});
});
}));
it("emits twice if a file was changed twice", fixtures((folder) => {
let summary = null;
return withGlobwatcher("**/*.x", { cwd: `${folder}` }, (g) => {
summary = capture(g);
fs.writeFileSync(`${folder}/one.x`, "whee1");
return g.check().then(() => {
summary.should.eql({
changed: [ `${folder}/one.x` ]
});
fs.writeFileSync(`${folder}/one.x`, "whee123");
return g.check();
}).then(() => {
summary.should.eql({
changed: [ `${folder}/one.x`, `${folder}/one.x` ]
});
});
});
}));
it("doesn't mind watching a nonexistent folder", fixtures((folder) => {
return withGlobwatcher(`${folder}/not/there/*`, (g) => {
(3).should.equal(3);
});
}));
it("sees a new matching file even if the whole folder was missing when it started", future(withTempFolder((folder) => {
let summary = null;
return withGlobwatcher(`${folder}/not/there/*`, (g) => {
summary = capture(g);
shell.mkdir("-p", `${folder}/not/there`);
fs.writeFileSync(`${folder}/not/there/ten.x`, "wheeeeeee");
return g.check().then(() => {
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((folder) => {
let summary = null;
return withGlobwatcher(`${folder}/sub/deeper/*.x`, (g) => {
summary = capture(g);
shell.mkdir("-p", `${folder}/sub/deeper`);
fs.writeFileSync(`${folder}/sub/deeper/ten.x`, "wheeeeeee");
return g.check().then(() => {
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((folder) => {
shell.rm("-rf", `${folder}/nested`);
shell.mkdir("-p", `${folder}/nested/deeper/still`);
touch.sync(`${folder}/nested/deeper/still/four.x`);
let summary = null;
return withGlobwatcher(`${folder}/**/*`, (g) => {
summary = capture(g);
shell.rm("-r", `${folder}/nested`);
return g.check().then(() => {
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(() => {
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((folder) => {
shell.mkdir("-p", `${folder}/nested/deeper`);
let summary = null;
return withGlobwatcher(`${folder}/nested/deeper/*.x`, (g) => {
summary = capture(g);
fs.writeFileSync(`${folder}/nested/deeper/ten.x`, "wheeeeeee");
return g.check().then(() => {
summary.should.eql({
added: [ `${folder}/nested/deeper/ten.x` ]
});
});
});
}));
it("emits signals for folders when asked", fixtures((folder) => {
let summary = null;
return withGlobwatcher(`${folder}/**/*`, { emitFolders: true }, (g) => {
summary = capture(g);
shell.mkdir("-p", `${folder}/newfolder`);
shell.rm("-r", `${folder}/nested`);
return g.check().then(() => {
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((folder) => {
let summary = null;
return withGlobwatcher(`${folder}/nothing.x`, (g) => {
summary = capture(g);
fs.writeFileSync(`${folder}/nothing.x`, "hi!");
return Promise.delay(g.interval).then(() => {
summary.should.eql({
added: [ `${folder}/nothing.x` ]
});
});
});
}));
it("returns a currentSet", fixtures((folder) => {
return withGlobwatcher(`${folder}/**/*.x`, (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(() => {
g.currentSet().sort().should.eql([
`${folder}/nested/three.x`,
`${folder}/one.x`,
`${folder}/sub/two.x`,
`${folder}/whatevs.x`
]);
});
});
}));
describe("takes a snapshot", () => {
it("of globs", fixtures((folder) => {
return withGlobwatcher(`${folder}/**/*.x`, { snapshot: {} }, (g) => {
let 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(() => {
let 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((folder) => {
return withGlobwatcher(`${folder}/sub/two.x`, { snapshot: {} }, (g) => {
let ts = fs.statSync(`${folder}/sub/two.x`).mtime.getTime();
return g.check().then(() => {
let 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 }, (g) => {
return g.check().then(() => {
snapshot = g.snapshot();
snapshot[`${folder}/sub/two.x`].should.eql({ mtime: ts, size: 4 });
});
});
});
});
}));
});
it("resumes from a snapshot", fixtures((folder) => {
return withGlobwatcher(`${folder}/**/*.x`, (g) => {
let summary = null;
let snapshot = g.snapshot();
g.close();
return Promise.delay(100).then(() => {
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(() => {
summary.should.eql({
added: [ `${folder}/sub/nine.x` ],
changed: [ `${folder}/one.x` ],
deleted: [ `${folder}/sub/two.x` ]
});
});
});
}));
});