diff --git a/lib/guard/listener.rb b/lib/guard/listener.rb index 0f8620b67..c226c350e 100644 --- a/lib/guard/listener.rb +++ b/lib/guard/listener.rb @@ -9,35 +9,80 @@ module Guard autoload :Polling, 'guard/listeners/polling' class Listener - attr_reader :last_event, :sha1_checksums_hash + attr_reader :last_event, :sha1_checksums_hash, :directory, :callback - def self.select_and_init + def self.select_and_init(*a) if mac? && Darwin.usable? - Darwin.new + Darwin.new(*a) elsif linux? && Linux.usable? - Linux.new + Linux.new(*a) elsif windows? && Windows.usable? - Windows.new + Windows.new(*a) else UI.info "Using polling (Please help us to support your system better than that.)" - Polling.new + Polling.new(*a) end end - def initialize + def initialize(directory=Dir.pwd, options={}) + @directory = directory.to_s @sha1_checksums_hash = {} + @relativate_paths = options.fetch(:relativate_paths, true) update_last_event end + def start + watch directory + end + + def stop + end + + def on_change(&callback) + @callback = callback + end + def update_last_event @last_event = Time.now end def modified_files(dirs, options = {}) +# <<<<<<< HEAD +# files = potentially_modified_files(dirs, options).select { |path| File.file?(path) && file_modified?(path) } +# files.map! { |file| file.gsub("#{Dir.pwd}/", '') } +# ======= files = potentially_modified_files(dirs, options).select { |path| File.file?(path) && file_modified?(path) } - files.map! { |file| file.gsub("#{Dir.pwd}/", '') } + relativate_paths files + end + + def worker + raise NotImplementedError, "should respond to #watch" + end + + # register a directory to watch. must be implemented by the subclasses + def watch(directory) + raise NotImplementedError, "do whatever you want here, given the directory as only argument" + end + + def all_files + potentially_modified_files [directory + '/'], :all => true +# >>>>>>> b12769d2bf385b3c69973721144cae3d5d8fbed9 end + # scopes all given paths to the current #directory + def relativate_paths(paths) + return paths unless relativate_paths? + paths.map do |path| + path.gsub(%r~^#{directory}/~, '') + end + end + + attr_writer :relativate_paths + def relativate_paths? + !!@relativate_paths + end + + private def potentially_modified_files(dirs, options = {}) diff --git a/lib/guard/listeners/darwin.rb b/lib/guard/listeners/darwin.rb index c6eb055c3..17bf48ebc 100644 --- a/lib/guard/listeners/darwin.rb +++ b/lib/guard/listeners/darwin.rb @@ -2,25 +2,23 @@ module Guard class Darwin < Listener attr_reader :fsevent - def initialize + def initialize(*) super @fsevent = FSEvent.new end - def on_change(&callback) - @fsevent.watch Dir.pwd do |modified_dirs| - files = modified_files(modified_dirs) - update_last_event - callback.call(files) - end + def worker + @fsevent end def start - @fsevent.run + super + fsevent.run end def stop - @fsevent.stop + super + fsevent.stop end def self.usable? @@ -36,5 +34,15 @@ def self.usable? false end + private + + def watch(directory) + worker.watch directory do |modified_dirs| + files = modified_files(modified_dirs) + update_last_event + callback.call(files) + end + end + end end diff --git a/lib/guard/listeners/linux.rb b/lib/guard/listeners/linux.rb index 2d53b6209..041ae31ed 100644 --- a/lib/guard/listeners/linux.rb +++ b/lib/guard/listeners/linux.rb @@ -1,8 +1,8 @@ module Guard class Linux < Listener - attr_reader :inotify, :files, :latency, :callback + attr_reader :inotify, :files, :latency - def initialize + def initialize(*) super @inotify = INotify::Notifier.new @@ -12,24 +12,16 @@ def initialize def start @stop = false + super watch_change unless watch_change? end def stop + super @stop = true sleep latency end - def on_change(&callback) - @callback = callback - inotify.watch(Dir.pwd, :recursive, :modify, :create, :delete, :move) do |event| - unless event.name == "" # Event on root directory - @files << event.absolute_name - end - end - rescue Interrupt - end - def self.usable? require 'rb-inotify' if !defined?(INotify::VERSION) || Gem::Version.new(INotify::VERSION.join('.')) < Gem::Version.new('0.5.1') @@ -49,6 +41,19 @@ def watch_change? private + def worker + @inotify + end + + def watch(directory) + worker.watch(directory, :recursive, :modify, :create) do |event| + unless event.name == "" # Event on root directory + @files << event.absolute_name + end + end + rescue Interrupt + end + def watch_change @watch_change = true until @stop @@ -61,8 +66,7 @@ def watch_change unless files.empty? files.uniq! - files.map! { |file| file.gsub("#{Dir.pwd}/", '') } - callback.call(files) + callback.call( relativate_paths(files) ) files.clear end end diff --git a/lib/guard/listeners/polling.rb b/lib/guard/listeners/polling.rb index bf5a17bfa..2f61a6d71 100644 --- a/lib/guard/listeners/polling.rb +++ b/lib/guard/listeners/polling.rb @@ -1,22 +1,20 @@ module Guard class Polling < Listener - attr_reader :callback, :latency + attr_reader :latency - def initialize + def initialize(*) super @latency = 1.5 end - def on_change(&callback) - @callback = callback - end - def start @stop = false + super watch_change end def stop + super @stop = true end @@ -27,11 +25,15 @@ def watch_change start = Time.now.to_f files = modified_files([Dir.pwd + '/'], :all => true) update_last_event - callback.call(files) unless files.empty? + callback.call(relativate_paths(files)) unless files.empty? nap_time = latency - (Time.now.to_f - start) sleep(nap_time) if nap_time > 0 end end + def watch(directory) + @existing = all_files + end + end end diff --git a/lib/guard/listeners/windows.rb b/lib/guard/listeners/windows.rb index e6d888a9d..b048ce446 100644 --- a/lib/guard/listeners/windows.rb +++ b/lib/guard/listeners/windows.rb @@ -7,20 +7,13 @@ def initialize @fchange = FChange::Notifier.new end - def on_change(&callback) - @fchange.watch(Dir.pwd, :all_events, :recursive) do |event| - paths = [File.expand_path(event.watcher.path) + '/'] - files = modified_files(paths, {:all => true}) - update_last_event - callback.call(files) - end - end - def start + super @fchange.run end def stop + super @fchange.stop end @@ -32,5 +25,20 @@ def self.usable? false end + private + + def worker + @fchange + end + + def watch(directory) + worker.watch(directory, :all_events, :recursive) do |event| + paths = [File.expand_path(event.watcher.path) + '/'] + files = modified_files(paths, {:all => true}) + update_last_event + callback.call(files) + end + end + end end diff --git a/spec/guard/listener_spec.rb b/spec/guard/listener_spec.rb index 5206a716c..ac3801c60 100644 --- a/spec/guard/listener_spec.rb +++ b/spec/guard/listener_spec.rb @@ -27,6 +27,39 @@ Guard::Linux.should_receive(:new) subject.select_and_init end + + it "forwards its arguments to the constructor" do + subject.stub!(:mac?).and_return(true) + Guard::Darwin.stub!(:usable?).and_return(true) + + path, opts = 'path', {:foo => 23} + Guard::Darwin.should_receive(:new).with(path, opts).and_return(true) + subject.select_and_init path, opts + end + end + + describe "#all_files" do + subject { described_class.new(@fixture_path) } + + it "should return all files" do + subject.all_files.should =~ Dir.glob("#{@fixture_path}/**/*") + end + end + + describe "#relativate_paths" do + subject { described_class.new('/tmp') } + before :each do + @paths = %w( /tmp/a /tmp/a/b /tmp/a.b/c.d ) + end + + it "should relativate paths to the configured directory" do + subject.relativate_paths(@paths).should =~ %w( a a/b a.b/c.d ) + end + + it "can be disabled" do + subject.relativate_paths = false + subject.relativate_paths(@paths).should == @paths + end end describe "#update_last_event" do @@ -91,4 +124,39 @@ end end + describe "working directory" do + + context "unspecified" do + subject { described_class.new } + it "defaults to Dir.pwd" do + subject.directory.should == Dir.pwd + end + it "can be not changed" do + subject.should_not respond_to(:directory=) + end + end + + context "specified as first argument to ::new" do + before :each do + @wd = @fixture_path.join("folder1") + end + subject { described_class.new @wd } + it "can be inspected" do + subject.directory.should == @wd.to_s + end + it "can be not changed" do + subject.should_not respond_to(:directory=) + end + + it "will be used to watch" do + subject.should_receive(:watch).with(@wd.to_s) + @listener = subject # indeed. + start + stop + end + end + + end + + end diff --git a/spec/guard/listeners/darwin_spec.rb b/spec/guard/listeners/darwin_spec.rb index c6d54eb57..25c26f36c 100644 --- a/spec/guard/listeners/darwin_spec.rb +++ b/spec/guard/listeners/darwin_spec.rb @@ -4,6 +4,12 @@ describe Guard::Darwin do subject { Guard::Darwin } + if windows? + it "isn't usable on windows" do + subject.should_not be_usable + end + end + if linux? it "isn't usable on linux" do subject.should_not be_usable @@ -15,61 +21,9 @@ subject.should be_usable end - describe "#on_change" do - before(:each) do - @results = [] - @listener = Guard::Darwin.new - @listener.on_change do |files| - @results += files - end - end - - it "catches a new file" do - file = @fixture_path.join("newfile.rb") - File.exists?(file).should be_false - start - FileUtils.touch file - stop - File.delete file - @results.should == ['spec/fixtures/newfile.rb'] - end - - it "catches a single file update" do - file = @fixture_path.join("folder1/file1.txt") - File.exists?(file).should be_true - start - FileUtils.touch file - stop - @results.should == ['spec/fixtures/folder1/file1.txt'] - end - - it "catches multiple file updates" do - file1 = @fixture_path.join("folder1/file1.txt") - file2 = @fixture_path.join("folder1/folder2/file2.txt") - File.exists?(file1).should be_true - File.exists?(file2).should be_true - start - FileUtils.touch file1 - FileUtils.touch file2 - stop - @results.should == ['spec/fixtures/folder1/file1.txt', 'spec/fixtures/folder1/folder2/file2.txt'] - end - end - end - -private - - def start - sleep 1 - @listener.update_last_event - Thread.new { @listener.start } - sleep 1 - end + it_should_behave_like "a listener that reacts to #on_change" + it_should_behave_like "a listener scoped to a specific directory" - def stop - sleep 1 - @listener.stop - sleep 1 end end diff --git a/spec/guard/listeners/linux_spec.rb b/spec/guard/listeners/linux_spec.rb index 1f7c27e29..fca924c5c 100644 --- a/spec/guard/listeners/linux_spec.rb +++ b/spec/guard/listeners/linux_spec.rb @@ -11,6 +11,12 @@ end end + if windows? + it "isn't usable on windows" do + subject.should_not be_usable + end + end + if linux? && Guard::Linux.usable? it "is usable on linux" do subject.should be_usable @@ -37,95 +43,24 @@ stop @listener.should_not be_watch_change end - end - describe "#on_change", :long_running=> true do - before(:each) do - @results = [] - @listener = Guard::Linux.new - @listener.on_change do |files| - @results += files - end - end - - it "catches a new file" do - file = @fixture_path.join("newfile.rb") - File.exists?(file).should be_false - start - FileUtils.touch file - stop - File.delete file - @results.should == ['fixtures/newfile.rb'] - end - - it "catches a single file update" do - file = @fixture_path.join("folder1/file1.txt") - File.exists?(file).should be_true - start - File.open(file, 'w') {|f| f.write('') } - stop - @results.should == ['fixtures/folder1/file1.txt'] - end - - it "catches multiple file updates" do - file1 = @fixture_path.join("folder1/file1.txt") - file2 = @fixture_path.join("folder1/folder2/file2.txt") - File.exists?(file1).should be_true - File.exists?(file2).should be_true - start - File.open(file1, 'w') {|f| f.write('') } - File.open(file2, 'w') {|f| f.write('') } - stop - @results.should == ['fixtures/folder1/file1.txt', 'fixtures/folder1/folder2/file2.txt'] - end + it_should_behave_like "a listener that reacts to #on_change" + it_should_behave_like "a listener scoped to a specific directory" - it "catches a deleted file" do - file = @fixture_path.join("folder1/file1.txt") - File.exists?(file).should be_true - start - File.delete file - stop - FileUtils.touch file - @results.should == ['fixtures/folder1/file1.txt'] - end + # Fun fact: File.touch seems not to be enough on Linux to trigger a modify event - it "catches a moved file" do - file1 = @fixture_path.join("folder1/file1.txt") - file2 = @fixture_path.join("folder1/movedfile1.txt") - File.exists?(file1).should be_true - File.exists?(file2).should be_false - start - FileUtils.mv file1, file2 - stop - FileUtils.mv file2, file1 - @results.should == ['fixtures/folder1/file1.txt', 'fixtures/folder1/movedfile1.txt'] - end - - it "doesn't process a change when it is stopped" do - file = @fixture_path.join("folder1/file1.txt") - File.exists?(file).should be_true - start - @listener.inotify.should_not_receive(:process) - stop - File.open(file, 'w') {|f| f.write('') } - end + it "doesn't process a change when it is stopped" do + @listener = described_class.new + record_results + file = @fixture_path.join("folder1/file1.txt") + File.exists?(file).should be_true + start + @listener.inotify.should_not_receive(:process) + stop + File.open(file, 'w') {|f| f.write('') } end - end - -private - - def start - sleep 1 - @listener.update_last_event - Thread.new { @listener.start } - sleep 1 - end - def stop - sleep 1 - @listener.stop - sleep 1 end end diff --git a/spec/guard/listeners/polling_spec.rb b/spec/guard/listeners/polling_spec.rb index 258783c70..90d389f77 100644 --- a/spec/guard/listeners/polling_spec.rb +++ b/spec/guard/listeners/polling_spec.rb @@ -3,60 +3,9 @@ describe Guard::Polling do - before(:each) do - @results = [] - @listener = Guard::Polling.new - @listener.on_change do |files| - @results += files - end - end + subject { Guard::Polling } - describe "#on_change" do - it "catches a new file" do - file = @fixture_path.join("newfile.rb") - File.exists?(file).should be_false - start - FileUtils.touch file - stop - File.delete file - @results.should == ['spec/fixtures/newfile.rb'] - end - - it "catches a single file update" do - file = @fixture_path.join("folder1/file1.txt") - File.exists?(file).should be_true - start - FileUtils.touch file - stop - @results.should == ['spec/fixtures/folder1/file1.txt'] - end - - it "catches multiple file updates" do - file1 = @fixture_path.join("folder1/file1.txt") - file2 = @fixture_path.join("folder1/folder2/file2.txt") - File.exists?(file1).should be_true - File.exists?(file2).should be_true - start - FileUtils.touch file1 - FileUtils.touch file2 - stop - @results.should =~ ['spec/fixtures/folder1/file1.txt', 'spec/fixtures/folder1/folder2/file2.txt'] - end - end - -private - - def start - sleep 1 - @listener.update_last_event - Thread.new { @listener.start } - sleep 1 - end - - def stop - sleep 1 - @listener.stop - sleep 1 - end + it_should_behave_like "a listener that reacts to #on_change" + it_should_behave_like "a listener scoped to a specific directory" end diff --git a/spec/guard/listeners/windows_spec.rb b/spec/guard/listeners/windows_spec.rb index b0d945e10..133dc8396 100644 --- a/spec/guard/listeners/windows_spec.rb +++ b/spec/guard/listeners/windows_spec.rb @@ -21,70 +21,9 @@ subject.should be_usable end - describe "#on_change" do - before(:each) do - @results = [] - @listener = Guard::Windows.new - @listener.on_change do |files| - @results += files - end - end + it_should_behave_like "a listener that reacts to #on_change" + it_should_behave_like "a listener scoped to a specific directory" - it "catches a new file" do - file = @fixture_path.join("newfile.rb") - if File.exists?(file) - begin - File.delete file - rescue - end - end - File.exists?(file).should be_false - start - FileUtils.touch file - stop - begin - File.delete file - rescue - end - @results.should == ['spec/fixtures/newfile.rb'] - end - - it "catches a single file update" do - file = @fixture_path.join("folder1/file1.txt") - File.exists?(file).should be_true - start - FileUtils.touch file - stop - @results.should == ['spec/fixtures/folder1/file1.txt'] - end - - it "catches multiple file updates" do - file1 = @fixture_path.join("folder1/file1.txt") - file2 = @fixture_path.join("folder1/folder2/file2.txt") - File.exists?(file1).should be_true - File.exists?(file2).should be_true - start - FileUtils.touch file1 - FileUtils.touch file2 - stop - @results.should == ['spec/fixtures/folder1/file1.txt', 'spec/fixtures/folder1/folder2/file2.txt'] - end - end - end - -private - - def start - sleep 1 - @listener.update_last_event - Thread.new { @listener.start } - sleep 1 - end - - def stop - sleep 1 - @listener.stop - sleep 1 end end diff --git a/spec/support/listener_helper.rb b/spec/support/listener_helper.rb new file mode 100644 index 000000000..897f57300 --- /dev/null +++ b/spec/support/listener_helper.rb @@ -0,0 +1,116 @@ +private + + def start + sleep 1 + @listener.update_last_event + Thread.new { @listener.start } + sleep 1 + end + + def record_results + @results = [] + @listener.on_change do |files| + @results += files + end + end + + def stop + sleep 1 + @listener.stop + sleep 1 + end + + def results + @results.flatten + end + +shared_examples_for 'a listener that reacts to #on_change' do + before(:each) do + @listener = described_class.new + record_results + end + + it "catches a new file" do + file = @fixture_path.join("newfile.rb") + if File.exists?(file) + begin + File.delete file + rescue + end + end + File.exists?(file).should be_false + start + FileUtils.touch file + stop + begin + File.delete file + rescue + end + results.should =~ ['spec/fixtures/newfile.rb'] + end + + it "catches a single file update" do + file = @fixture_path.join("folder1/file1.txt") + File.exists?(file).should be_true + start + File.open(file, 'w') {|f| f.write('') } + stop + results.should =~ ['spec/fixtures/folder1/file1.txt'] + end + + it "catches multiple file updates" do + file1 = @fixture_path.join("folder1/file1.txt") + file2 = @fixture_path.join("folder1/folder2/file2.txt") + File.exists?(file1).should be_true + File.exists?(file2).should be_true + start + File.open(file1, 'w') {|f| f.write('') } + File.open(file2, 'w') {|f| f.write('') } + stop + results.should =~ ['spec/fixtures/folder1/file1.txt', 'spec/fixtures/folder1/folder2/file2.txt'] + end + + it "not catches a deleted file" do + file = @fixture_path.join("folder1/file1.txt") + File.exists?(file).should be_true + start + File.delete file + stop + FileUtils.touch file + results.should =~ [] + end + + it "not catches a moved file" do + file1 = @fixture_path.join("folder1/file1.txt") + file2 = @fixture_path.join("folder1/movedfile1.txt") + File.exists?(file1).should be_true + File.exists?(file2).should be_false + start + FileUtils.mv file1, file2 + stop + FileUtils.mv file2, file1 + results.should =~ [] + end + +end + +shared_examples_for "a listener scoped to a specific directory" do + before :each do + @wd = @fixture_path.join("folder1") + @listener = described_class.new @wd + end + it "should base paths within this directory" do + record_results + new_file = @wd.join("folder2/newfile.rb") + modified = @wd.join("file1.txt") + File.exists?(modified).should be_true + File.exists?(new_file).should be_false + start + FileUtils.touch new_file + File.open(modified, 'w') {|f| f.write('') } + stop + File.delete new_file + results.should =~ ["folder2/newfile.rb", 'file1.txt'] + end +end +