UNPKG

globwatcher

Version:

watch a set of files for changes (including create/delete) by glob patterns

366 lines (331 loc) 13.6 kB
fs = require 'fs' minimatch = require 'minimatch' path = require 'path' Q = require 'q' shell = require 'shelljs' should = require 'should' touch = require 'touch' util = require 'util' test_util = require("./test_util") futureTest = test_util.futureTest withTempFolder = test_util.withTempFolder globwatcher = require("../lib/globwatcher/globwatcher") dump = (x) -> util.inspect x, false, null, true makeFixtures = (folder, ts) -> if not ts? then 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 fixtures = (f) -> futureTest withTempFolder (folder) -> makeFixtures(folder) f(folder) # create a new globwatch, run a test, and close it withGlobwatcher = (pattern, options, f) -> if not f? f = options options = {} options.persistent = false g = globwatcher.globwatcher(pattern, options) g.ready.fin -> f(g) .fin -> g.close() # capture add/remove/change into an object for later inspection capture = (g) -> summary = {} g.on "added", (filename) -> (summary["added"] or= []).push filename summary["added"].sort() g.on "deleted", (filename) -> (summary["deleted"] or= []).push filename summary["deleted"].sort() g.on "changed", (filename) -> (summary["changed"] or= []).push filename summary["changed"].sort() summary describe "globwatcher", -> it "folderMatchesMinimatchPrefix", -> 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", futureTest -> withGlobwatcher "/wut", (g) -> for f in [ "/absolute.txt" "/sub/absolute.txt" "/deeply/nested/file/why/nobody/knows.txt" ] then 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) -> 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) -> 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) -> 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) -> summary = null withGlobwatcher "#{folder}/**/*.x", (g) -> summary = capture(g) touch.sync "#{folder}/nested/four.x" touch.sync "#{folder}/sub/not-me.txt" g.check().then -> summary.should.eql { added: [ "#{folder}/nested/four.x" ] } it "doesn't emit signals when turned off", fixtures (folder) -> summary = null withGlobwatcher "#{folder}/**/*.x", (g) -> summary = capture(g) g.stopWatches() Q.delay(25).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 Q.delay(g.interval * 2) .then -> summary.should.eql { } it "notices new files only in cwd", fixtures (folder) -> summary = null 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" g.check().then -> summary.should.eql { added: [ "#{folder}/sub/four.x" ] } it "notices new files nested deeply", fixtures (folder) -> summary = null withGlobwatcher "#{folder}/**/*.x", (g) -> summary = capture(g) shell.mkdir "-p", "#{folder}/nested/more/deeply" touch.sync "#{folder}/nested/more/deeply/nine.x" g.check().then -> summary.should.eql { added: [ "#{folder}/nested/more/deeply/nine.x" ] } it "notices deleted files", fixtures (folder) -> summary = null withGlobwatcher "**/*.x", { cwd: "#{folder}" }, (g) -> summary = capture(g) fs.unlinkSync("#{folder}/sub/one.x") g.check().then -> summary.should.eql { deleted: [ "#{folder}/sub/one.x" ] } it "notices a rename as an add + delete", fixtures (folder) -> summary = null withGlobwatcher "**/*.x", { cwd: "#{folder}" }, (g) -> summary = capture(g) fs.renameSync "#{folder}/sub/two.x", "#{folder}/sub/twelve.x" 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" summary = null withGlobwatcher "**/*.x", { cwd: "#{folder}" }, (g) -> summary = capture(g) shell.rm "-r", "#{folder}/nested" g.check().then -> summary.should.eql { deleted: [ "#{folder}/nested/more/deeply/here.x", "#{folder}/nested/three.x" ] } it "handles a changed file", fixtures (folder) -> summary = null withGlobwatcher "**/*.x", { cwd: "#{folder}" }, (g) -> summary = capture(g) fs.writeFileSync "#{folder}/sub/one.x", "gahhhhhh" g.check().then -> summary.should.eql { changed: [ "#{folder}/sub/one.x" ] } it "follows a safe-write", fixtures (folder) -> summary = null savee = "#{folder}/one.x" backup = "#{folder}/one.x~" withGlobwatcher "**/*.x", { cwd: "#{folder}" }, (g) -> summary = capture(g) fs.writeFileSync backup, fs.readFileSync(savee) fs.unlinkSync savee fs.renameSync backup, savee g.check().then -> summary.should.eql { changed: [ savee ] } it "only emits once for a changed file", fixtures (folder) -> summary = null withGlobwatcher "**/*.x", { cwd: "#{folder}" }, (g) -> summary = capture(g) fs.writeFileSync "#{folder}/one.x", "whee1" g.check().then -> summary.should.eql { changed: [ "#{folder}/one.x" ] } g.check() .then -> summary.should.eql { changed: [ "#{folder}/one.x" ] } it "emits twice if a file was changed twice", fixtures (folder) -> summary = null withGlobwatcher "**/*.x", { cwd: "#{folder}" }, (g) -> summary = capture(g) fs.writeFileSync "#{folder}/one.x", "whee1" g.check().then -> summary.should.eql { changed: [ "#{folder}/one.x" ] } fs.writeFileSync "#{folder}/one.x", "whee123" g.check() .then -> summary.should.eql { changed: [ "#{folder}/one.x", "#{folder}/one.x" ] } it "doesn't mind watching a nonexistent folder", fixtures (folder) -> 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", futureTest withTempFolder (folder) -> summary = null withGlobwatcher "#{folder}/not/there/*", (g) -> summary = capture(g) shell.mkdir "-p", "#{folder}/not/there" fs.writeFileSync "#{folder}/not/there/ten.x", "wheeeeeee" 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) -> summary = null withGlobwatcher "#{folder}/sub/deeper/*.x", (g) -> summary = capture(g) shell.mkdir "-p", "#{folder}/sub/deeper" fs.writeFileSync "#{folder}/sub/deeper/ten.x", "wheeeeeee" 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" summary = null withGlobwatcher "#{folder}/**/*", (g) -> summary = capture(g) shell.rm "-r", "#{folder}/nested" 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" 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" summary = null withGlobwatcher "#{folder}/nested/deeper/*.x", (g) -> summary = capture(g) fs.writeFileSync "#{folder}/nested/deeper/ten.x", "wheeeeeee" g.check().then -> summary.should.eql { added: [ "#{folder}/nested/deeper/ten.x" ] } it "will watch a single, non-globbed file that doesn't exist", fixtures (folder) -> summary = null withGlobwatcher "#{folder}/nothing.x", (g) -> summary = capture(g) fs.writeFileSync "#{folder}/nothing.x", "hi!" Q.delay(g.interval).then -> summary.should.eql { added: [ "#{folder}/nothing.x" ] } it "returns a currentSet", fixtures (folder) -> 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" g.check().then -> g.currentSet().sort().should.eql [ "#{folder}/nested/three.x" "#{folder}/one.x" "#{folder}/sub/two.x" "#{folder}/whatevs.x" ] it "takes a snapshot", fixtures (folder) -> withGlobwatcher "#{folder}/**/*.x", (g) -> ts = fs.statSync("#{folder}/one.x").mtime.getTime() fs.writeFileSync "#{folder}/wut.x", "hello" touch.sync "#{folder}/wut.x", mtime: ts g.check().then -> 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 "resumes from a snapshot", fixtures (folder) -> withGlobwatcher "#{folder}/**/*.x", (g) -> summary = null snapshot = g.snapshot() g.close() Q.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) g.ready .then -> summary.should.eql { added: [ "#{folder}/sub/nine.x" ] changed: [ "#{folder}/one.x" ] deleted: [ "#{folder}/sub/two.x" ] }