From c6bb44fa6710349fb6acb224a1e5385351c8e14d Mon Sep 17 00:00:00 2001 From: Jason Woods Date: Tue, 5 May 2015 09:13:51 +0100 Subject: [PATCH 01/10] Fix unintended amalgamation of root files and the directory list --- lib/listen/record.rb | 42 ++++++++++++++---------------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/lib/listen/record.rb b/lib/listen/record.rb index 3255fc45..44cc1684 100644 --- a/lib/listen/record.rb +++ b/lib/listen/record.rb @@ -15,7 +15,7 @@ def initialize(listener) end def add_dir(dir, rel_path) - return if [nil, '', '.'].include? rel_path + rel_path = '.' if [nil, '', '.'].include? rel_path @paths[dir.to_s][rel_path] ||= {} end @@ -32,23 +32,16 @@ def unset_path(dir, rel_path) def file_data(dir, rel_path) root = @paths[dir.to_s] dirname, basename = Pathname(rel_path).split.map(&:to_s) - if [nil, '', '.'].include? dirname - root[basename] ||= {} - root[basename].dup - else - root[dirname] ||= {} - root[dirname][basename] ||= {} - root[dirname][basename].dup - end + dirname = '.' if [nil, '', '.'].include? dirname + root[dirname] ||= {} + root[dirname][basename] ||= {} + root[dirname][basename].dup end def dir_entries(dir, rel_path) - tree = if [nil, '', '.'].include? rel_path.to_s - @paths[dir.to_s] - else - @paths[dir.to_s][rel_path.to_s] ||= _auto_hash - @paths[dir.to_s][rel_path.to_s] - end + rel_path = '.' if [nil, '', '.'].include? rel_path.to_s + @paths[dir.to_s][rel_path.to_s] ||= _auto_hash + tree = @paths[dir.to_s][rel_path.to_s] result = {} tree.each do |key, values| @@ -81,25 +74,18 @@ def _auto_hash def _fast_update_file(dir, dirname, basename, data) root = @paths[dir.to_s] - if [nil, '', '.'].include? dirname - root[basename] = (root[basename] || {}).merge(data) - else - root[dirname] ||= {} - root[dirname][basename] = (root[dirname][basename] || {}).merge(data) - end + dirname = '.' if [nil, '', '.'].include? dirname + root[dirname] ||= {} + root[dirname][basename] = (root[dirname][basename] || {}).merge(data) end def _fast_unset_path(dir, dirname, basename) root = @paths[dir.to_s] # this may need to be reworked to properly remove # entries from a tree, without adding non-existing dirs to the record - if [nil, '', '.'].include? dirname - return unless root.key?(basename) - root.delete(basename) - else - return unless root.key?(dirname) - root[dirname].delete(basename) - end + dirname = '.' if [nil, '', '.'].include? dirname + return unless root.key?(dirname) + root[dirname].delete(basename) end # TODO: test with a file name given From 2f8ad95170d122f0179135938f8476208dc05e57 Mon Sep 17 00:00:00 2001 From: Cezary Baginski Date: Tue, 5 May 2015 17:23:21 +0200 Subject: [PATCH 02/10] update tests and + fix Record build logic --- lib/listen/directory.rb | 2 - lib/listen/record.rb | 45 +++++++----- spec/lib/listen/directory_spec.rb | 7 +- spec/lib/listen/record_spec.rb | 116 +++++++++++++++++++++--------- 4 files changed, 109 insertions(+), 61 deletions(-) diff --git a/lib/listen/directory.rb b/lib/listen/directory.rb index e1185b4f..8787057c 100644 --- a/lib/listen/directory.rb +++ b/lib/listen/directory.rb @@ -7,8 +7,6 @@ def self.scan(queue, sync_record, dir, rel_path, options) previous = sync_record.dir_entries(dir, rel_path) - record.add_dir(dir, rel_path) - # TODO: use children(with_directory: false) path = dir + rel_path current = Set.new(path.children) diff --git a/lib/listen/record.rb b/lib/listen/record.rb index 44cc1684..6c5a3b03 100644 --- a/lib/listen/record.rb +++ b/lib/listen/record.rb @@ -11,12 +11,7 @@ class Record def initialize(listener) @listener = listener - @paths = _auto_hash - end - - def add_dir(dir, rel_path) - rel_path = '.' if [nil, '', '.'].include? rel_path - @paths[dir.to_s][rel_path] ||= {} + @paths = {} end def update_file(dir, rel_path, data) @@ -33,6 +28,8 @@ def file_data(dir, rel_path) root = @paths[dir.to_s] dirname, basename = Pathname(rel_path).split.map(&:to_s) dirname = '.' if [nil, '', '.'].include? dirname + fail "directory not watched: #{dir}" unless root + root[dirname] ||= {} root[dirname][basename] ||= {} root[dirname][basename].dup @@ -40,20 +37,12 @@ def file_data(dir, rel_path) def dir_entries(dir, rel_path) rel_path = '.' if [nil, '', '.'].include? rel_path.to_s - @paths[dir.to_s][rel_path.to_s] ||= _auto_hash - tree = @paths[dir.to_s][rel_path.to_s] - - result = {} - tree.each do |key, values| - # only get data for file entries - result[key] = values.key?(:mtime) ? values : {} - end - result + @paths[dir.to_s][rel_path.to_s] ||= {} end def build start = Time.now.to_f - @paths = _auto_hash + @paths = {} # TODO: refactor this out (1 Record = 1 watched dir) listener.directories.each do |directory| @@ -68,13 +57,31 @@ def build private - def _auto_hash - Hash.new { |h, k| h[k] = Hash.new } + # TODO: refactor/refactor out + def add_dir(dir, rel_path) + rel_path = '.' if [nil, '', '.'].include? rel_path + dirname, basename = Pathname(rel_path).split.map(&:to_s) + basename = '.' if [nil, '', '.'].include? basename + root = (@paths[dir.to_s] ||= {}) + if [nil, '', '.'].include?(dirname) + entries = (root['.'] || {}) + entries.merge!(basename => {}) if basename != '.' + root['.'] = entries + else + root[rel_path] ||= {} + end end def _fast_update_file(dir, dirname, basename, data) root = @paths[dir.to_s] dirname = '.' if [nil, '', '.'].include? dirname + + internal_dirname, internal_key = ::File.split(dirname.to_s) + if internal_dirname == '.' && internal_key != '.' + root[internal_dirname] ||= {} + root[internal_dirname][internal_key] ||= {} + end + root[dirname] ||= {} root[dirname][basename] = (root[dirname][basename] || {}).merge(data) end @@ -93,7 +100,7 @@ def _fast_unset_path(dir, dirname, basename) # TODO: test with mixed encoding def _fast_build(root) symlink_detector = SymlinkDetector.new - @paths[root] = _auto_hash + @paths[root] = {} remaining = Queue.new remaining << Entry.new(root, nil, nil) _fast_build_dir(remaining, symlink_detector) until remaining.empty? diff --git a/spec/lib/listen/directory_spec.rb b/spec/lib/listen/directory_spec.rb index f77e98ce..46f0f6d0 100644 --- a/spec/lib/listen/directory_spec.rb +++ b/spec/lib/listen/directory_spec.rb @@ -9,7 +9,7 @@ let(:queue) { instance_double(Change, change: nil) } let(:async_record) do - instance_double(Record, add_dir: true, unset_path: true) + instance_double(Record, unset_path: true) end let(:record) do @@ -37,11 +37,6 @@ context 'with empty dir' do before { allow(dir).to receive(:children) { [] } } - it 'sets record dir path' do - expect(async_record).to receive(:add_dir).with(dir, '.') - described_class.scan(queue, record, dir, '.', options) - end - it "queues changes for file path and dir that doesn't exist" do expect(queue).to receive(:change).with(:file, dir, 'file.rb') diff --git a/spec/lib/listen/record_spec.rb b/spec/lib/listen/record_spec.rb index 8e2ea9e0..14aa954e 100644 --- a/spec/lib/listen/record_spec.rb +++ b/spec/lib/listen/record_spec.rb @@ -45,31 +45,39 @@ def symlink(hash_or_dir) end end + def build_watched_dir(record, dir) + record.send(:add_dir, dir, '.') + end + let(:record) { Listen::Record.new(listener) } let(:dir) { instance_double(Pathname, to_s: '/dir') } describe '#update_file' do + before { build_watched_dir(record, dir) } + context 'with path in watched dir' do it 'sets path by spliting dirname and basename' do record.update_file(dir, 'file.rb', mtime: 1.1) - expect(record.paths['/dir']).to eq('file.rb' => { mtime: 1.1 }) + expect(record.paths['/dir']['.']).to eq('file.rb' => { mtime: 1.1 }) end it 'sets path and keeps old data not overwritten' do record.update_file(dir, 'file.rb', foo: 1, bar: 2) record.update_file(dir, 'file.rb', foo: 3) watched_dir = record.paths['/dir'] - expect(watched_dir).to eq('file.rb' => { foo: 3, bar: 2 }) + expect(watched_dir['.']).to eq('file.rb' => { foo: 3, bar: 2 }) end end context 'with subdir path' do it 'sets path by spliting dirname and basename' do + record.send(:add_dir, dir, 'path') record.update_file(dir, 'path/file.rb', mtime: 1.1) expect(record.paths['/dir']['path']).to eq('file.rb' => { mtime: 1.1 }) end it 'sets path and keeps old data not overwritten' do + record.send(:add_dir, dir, 'path') record.update_file(dir, 'path/file.rb', foo: 1, bar: 2) record.update_file(dir, 'path/file.rb', foo: 3) file_data = record.paths['/dir']['path']['file.rb'] @@ -78,36 +86,40 @@ def symlink(hash_or_dir) end end + # TODO: refactor/refactor out describe '#add_dir' do it 'sets itself when .' do - record.add_dir(dir, '.') - expect(record.paths['/dir']).to eq({}) + record.send(:add_dir, dir, '.') + expect(record.paths['/dir']['.']).to eq({}) end it 'sets itself when nil' do - record.add_dir(dir, nil) - expect(record.paths['/dir']).to eq({}) + record.send(:add_dir, dir, nil) + expect(record.paths['/dir']['.']).to eq({}) end it 'sets itself when empty' do - record.add_dir(dir, '') - expect(record.paths['/dir']).to eq({}) + record.send(:add_dir, dir, '') + expect(record.paths['/dir']['.']).to eq({}) end it 'correctly sets new directory data' do - record.add_dir(dir, 'path/subdir') - expect(record.paths['/dir']).to eq('path/subdir' => {}) + record.send(:add_dir, dir, '.') + record.send(:add_dir, dir, 'path/subdir') + expect(record.paths['/dir']).to eq('.' => {}, 'path/subdir' => {}) end it 'sets path and keeps old data not overwritten' do - record.add_dir(dir, 'path/subdir') + record.send(:add_dir, dir, '.') + record.send(:add_dir, dir, 'path') + record.send(:add_dir, dir, 'path/subdir') record.update_file(dir, 'path/subdir/file.rb', mtime: 1.1) - record.add_dir(dir, 'path/subdir') + record.send(:add_dir, dir, 'path/subdir') record.update_file(dir, 'path/subdir/file2.rb', mtime: 1.2) - record.add_dir(dir, 'path/subdir') + record.send(:add_dir, dir, 'path/subdir') watched = record.paths['/dir'] - expect(watched.keys).to eq ['path/subdir'] + expect(watched.keys).to eq ['.', 'path/subdir'] expect(watched['path/subdir'].keys).to eq %w(file.rb file2.rb) subdir = watched['path/subdir'] @@ -117,20 +129,22 @@ def symlink(hash_or_dir) end describe '#unset_path' do + before { build_watched_dir(record, dir) } + context 'within watched dir' do context 'when path is present' do before { record.update_file(dir, 'file.rb', mtime: 1.1) } it 'unsets path' do record.unset_path(dir, 'file.rb') - expect(record.paths).to eq('/dir' => {}) + expect(record.paths['/dir']).to eq('.' => {}) end end context 'when path not present' do it 'unsets path' do record.unset_path(dir, 'file.rb') - expect(record.paths).to eq('/dir' => {}) + expect(record.paths['/dir']).to eq('.' => {}) end end end @@ -141,20 +155,21 @@ def symlink(hash_or_dir) it 'unsets path' do record.unset_path(dir, 'path/file.rb') - expect(record.paths).to eq('/dir' => { 'path' => {} }) + expect(record.paths['/dir']).to eq('.' => {'path' => {}}, 'path' => {} ) end end context 'when path not present' do it 'unsets path' do record.unset_path(dir, 'path/file.rb') - expect(record.paths).to eq('/dir' => {}) + expect(record.paths['/dir']).to eq('.' => {}) end end end end describe '#file_data' do + before { build_watched_dir(record, dir) } context 'with path in watched dir' do context 'when path is present' do before { record.update_file(dir, 'file.rb', mtime: 1.1) } @@ -190,6 +205,8 @@ def symlink(hash_or_dir) end describe '#dir_entries' do + before { build_watched_dir(record, dir) } + context 'in watched dir' do subject { record.dir_entries(dir, '.') } @@ -203,8 +220,28 @@ def symlink(hash_or_dir) end context 'with subdir/file.rb in record' do - before { record.update_file(dir, 'subdir/file.rb', mtime: 1.1) } - it { should eq('subdir' => {}) } + before do + record.update_file(dir, 'subdir', mtime: 1.0) + record.update_file(dir, 'subdir/file.rb', mtime: 1.1) + end + + it do + should eq('subdir' => { mtime: 1.0 }) + end + end + + context 'with tree and root files in record' do + before do + record.update_file(dir, 'file1.rb', mtime: 1.1) + record.update_file(dir, 'subdir', mtime: 1.2) + record.update_file(dir, 'subdir/file2.rb', mtime: 1.4) + end + it do + should eq( + 'file1.rb' => { mtime: 1.1 }, + 'subdir' => { mtime: 1.2 } + ) + end end end @@ -249,10 +286,12 @@ def symlink(hash_or_dir) real_directory('/dir1' => []) real_directory('/dir2' => []) - record.update_file(dir, 'path/file.rb', mtime: 1.1) + build_watched_dir(record, '/dir1') + record.update_file('/dir1', 'path/file.rb', mtime: 1.1) record.build - expect(record.paths).to eq('/dir1' => {}, '/dir2' => {}) - expect(record.file_data(dir, 'path/file.rb')).to be_empty + expect(record.paths['/dir1']).to eq('.' => {}) + expect(record.paths['/dir2']).to eq('.' => {}) + expect(record.file_data('/dir1', 'path/file.rb')).to be_empty end let(:foo_stat) { instance_double(::File::Stat, mtime: 1.0, mode: 0644) } @@ -271,8 +310,10 @@ def symlink(hash_or_dir) expect(record.paths.keys).to eq %w( /dir1 /dir2 ) expect(record.paths['/dir1']). to eq( - 'foo' => { mtime: 1.0, mode: 0644 }, - 'bar' => { mtime: 2.3, mode: 0755 }) + '.' => { + 'foo' => { mtime: 1.0, mode: 0644 }, + 'bar' => { mtime: 2.3, mode: 0755 } + }) end end @@ -287,9 +328,17 @@ def symlink(hash_or_dir) it 'builds record' do record.build expect(record.paths.keys).to eq %w( /dir1 /dir2 ) - expect(record.paths['/dir1']). - to eq('foo' => { 'bar' => { mtime: 2.3, mode: 0755 } }) - expect(record.paths['/dir2']).to eq({}) + expect(record.paths['/dir1']).to eq( + '.' => {'foo' => {}}, + 'foo' => { 'bar' => { mtime: 2.3, mode: 0755 } } + ) + expect(record.paths['/dir2']['.']).to eq({}) + end + + it 'properly shows list of children' do + record.build + expect(record.dir_entries('/dir1', 'foo')).to eq('bar' => {mtime: 2.3, mode: 0755}) + expect(record.dir_entries('/dir1', '.')).to eq('foo' => {}) end end @@ -307,13 +356,12 @@ def symlink(hash_or_dir) it 'builds record' do record.build expect(record.paths.keys).to eq %w( /dir1 /dir2 ) - expect(record.paths['/dir1']). - to eq( - 'foo' => {}, - 'foo/bar' => {}, - 'foo/baz' => {} + expect(record.paths['/dir1']).to eq( + '.' => {'foo' => {}}, + 'foo/bar' => {}, + 'foo/baz' => {} ) - expect(record.paths['/dir2']).to eq({}) + expect(record.paths['/dir2']).to eq('.' => {}) end end From 6b0006894e1ab3dd276999b85925983881da02c7 Mon Sep 17 00:00:00 2001 From: Cezary Baginski Date: Sat, 30 May 2015 15:44:01 +0200 Subject: [PATCH 03/10] rework Adapter specs --- spec/lib/listen/adapter_spec.rb | 83 +++++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 24 deletions(-) diff --git a/spec/lib/listen/adapter_spec.rb b/spec/lib/listen/adapter_spec.rb index d1b447d1..ff0164ac 100644 --- a/spec/lib/listen/adapter_spec.rb +++ b/spec/lib/listen/adapter_spec.rb @@ -9,38 +9,73 @@ end describe '.select' do - it 'returns TCP adapter when requested' do - klass = Listen::Adapter.select(force_tcp: true) - expect(klass).to eq Listen::Adapter::TCP - end + let(:options) { {} } + subject { Listen::Adapter.select(options) } - it 'returns Polling adapter if forced' do - klass = Listen::Adapter.select(force_polling: true) - expect(klass).to eq Listen::Adapter::Polling - end + context "when on Darwin" do + before { allow(Listen::Adapter::Darwin).to receive(:usable?) { true } } + + it { is_expected.to be Listen::Adapter::Darwin } - it 'returns BSD adapter when usable' do - allow(Listen::Adapter::BSD).to receive(:usable?) { true } - klass = Listen::Adapter.select - expect(klass).to eq Listen::Adapter::BSD + context "when TCP is requested" do + let(:options) { { force_tcp: true } } + it { is_expected.to be Listen::Adapter::TCP } + end + + context "when polling is forced" do + let(:options) { { force_polling: true } } + it { is_expected.to be Listen::Adapter::Polling } + end end - it 'returns Darwin adapter when usable' do - allow(Listen::Adapter::Darwin).to receive(:usable?) { true } - klass = Listen::Adapter.select - expect(klass).to eq Listen::Adapter::Darwin + context "when on BSD" do + before { allow(Listen::Adapter::BSD).to receive(:usable?) { true } } + + it { is_expected.to be Listen::Adapter::BSD } + + context "when TCP is requested" do + let(:options) { { force_tcp: true } } + it { is_expected.to be Listen::Adapter::TCP } + end + + context "when polling is forced" do + let(:options) { { force_polling: true } } + it { is_expected.to be Listen::Adapter::Polling } + end end - it 'returns Linux adapter when usable' do - allow(Listen::Adapter::Linux).to receive(:usable?) { true } - klass = Listen::Adapter.select - expect(klass).to eq Listen::Adapter::Linux + context "when on Linux" do + before { allow(Listen::Adapter::Linux).to receive(:usable?) { true } } + + it { is_expected.to be Listen::Adapter::Linux } + + context "when TCP is requested" do + let(:options) { { force_tcp: true } } + it { is_expected.to be Listen::Adapter::TCP } + end + + context "when polling is forced" do + let(:options) { { force_polling: true } } + it { is_expected.to be Listen::Adapter::Polling } + end end - it 'returns Windows adapter when usable' do - allow(Listen::Adapter::Windows).to receive(:usable?) { true } - klass = Listen::Adapter.select - expect(klass).to eq Listen::Adapter::Windows + context "when on Windows" do + before do + allow(Listen::Adapter::Windows).to receive(:usable?) { true } + end + + it { is_expected.to be Listen::Adapter::Windows } + + context "when TCP is requested" do + let(:options) { { force_tcp: true } } + it { is_expected.to be Listen::Adapter::TCP } + end + + context "when polling is forced" do + let(:options) { { force_polling: true } } + it { is_expected.to be Listen::Adapter::Polling } + end end context 'no usable adapters' do From 91bd5f20071ec6387fe45a488ffb60a64dd5417b Mon Sep 17 00:00:00 2001 From: Cezary Baginski Date: Sat, 30 May 2015 15:38:57 +0200 Subject: [PATCH 04/10] extract simulator to SimulatedDarwin --- lib/listen/adapter.rb | 3 +- lib/listen/adapter/linux.rb | 10 --- lib/listen/adapter/simulated_darwin.rb | 65 +++++++++++++++++++ spec/lib/listen/adapter/linux_spec.rb | 24 ++++--- .../listen/adapter/simulated_darwin_spec.rb | 56 ++++++++++++++++ spec/lib/listen/adapter_spec.rb | 15 ++++- 6 files changed, 148 insertions(+), 25 deletions(-) create mode 100644 lib/listen/adapter/simulated_darwin.rb create mode 100644 spec/lib/listen/adapter/simulated_darwin_spec.rb diff --git a/lib/listen/adapter.rb b/lib/listen/adapter.rb index 058b8f50..7168cd54 100644 --- a/lib/listen/adapter.rb +++ b/lib/listen/adapter.rb @@ -4,10 +4,11 @@ require 'listen/adapter/linux' require 'listen/adapter/polling' require 'listen/adapter/windows' +require 'listen/adapter/simulated_darwin' module Listen module Adapter - OPTIMIZED_ADAPTERS = [Darwin, Linux, BSD, Windows] + OPTIMIZED_ADAPTERS = [Darwin, SimulatedDarwin, Linux, BSD, Windows] POLLING_FALLBACK_MESSAGE = 'Listen will be polling for changes.'\ 'Learn more at https://github.com/guard/listen#listen-adapters.' diff --git a/lib/listen/adapter/linux.rb b/lib/listen/adapter/linux.rb index 72f5e005..2e55f458 100644 --- a/lib/listen/adapter/linux.rb +++ b/lib/listen/adapter/linux.rb @@ -49,16 +49,6 @@ def _process_event(dir, event) _log :debug, "inotify: #{rel_path} (#{event.flags.inspect})" - if /1|true/ =~ ENV['LISTEN_GEM_SIMULATE_FSEVENT'] - if (event.flags & [:moved_to, :moved_from]) || _dir_event?(event) - rel_path = path.dirname.relative_path_from(dir).to_s - _queue_change(:dir, dir, rel_path, {}) - else - _queue_change(:dir, dir, rel_path, {}) - end - return - end - return if _skip_event?(event) cookie_params = event.cookie.zero? ? {} : { cookie: event.cookie } diff --git a/lib/listen/adapter/simulated_darwin.rb b/lib/listen/adapter/simulated_darwin.rb new file mode 100644 index 00000000..3078b684 --- /dev/null +++ b/lib/listen/adapter/simulated_darwin.rb @@ -0,0 +1,65 @@ +module Listen + module Adapter + class SimulatedDarwin < Linux + def self.usable? + os = RbConfig::CONFIG['target_os'] + return false unless const_get('OS_REGEXP') =~ os + /1|true/ =~ ENV['LISTEN_GEM_SIMULATE_FSEVENT'] + end + + class FakeEvent + attr_reader :dir + + def initialize(watched_dir, event) + # NOTE: avoid using event.absolute_name since new API + # will need to have a custom recursion implemented + # to properly match events to configured directories + @real_path = full_path(event).relative_path_from(watched_dir) + @dir = "#{Pathname(watched_dir) + dir_for_event(event, @real_path)}/" + end + + def real_path + @real_path.to_s + end + + private + + def dir?(event) + event.flags.include?(:isdir) + end + + def moved?(event) + (event.flags & [:moved_to, :moved_from]) + end + + def dir_for_event(event, rel_path) + (moved?(event) || dir?(event)) ? rel_path.dirname : rel_path + end + + def full_path(event) + Pathname.new(event.watcher.path) + event.name + end + end + + private + + def _process_event(watched_dir, event) + ev = FakeEvent.new(watched_dir, event) + + _log( + :debug, + "fake_fsevent: #{ev.dir}(#{ev.real_path}=#{event.flags.inspect})") + + _darwin.send(:_process_event, watched_dir, [ev.dir]) + end + + def _darwin + @darwin ||= Class.new(Darwin) do + def _configure(*_args) + # Skip FSEvent setup + end + end.new(mq: @mq) + end + end + end +end diff --git a/spec/lib/listen/adapter/linux_spec.rb b/spec/lib/listen/adapter/linux_spec.rb index 3cbec225..b29a8da2 100644 --- a/spec/lib/listen/adapter/linux_spec.rb +++ b/spec/lib/listen/adapter/linux_spec.rb @@ -70,21 +70,19 @@ end # TODO: get fsevent adapter working like INotify - unless /1|true/ =~ ENV['LISTEN_GEM_SIMULATE_FSEVENT'] - it 'recognizes close_write as modify' do - expect_change.call(:modified) - event_callback.call([:close_write]) - end + it 'recognizes close_write as modify' do + expect_change.call(:modified) + event_callback.call([:close_write]) + end - it 'recognizes moved_to as moved_to' do - expect_change.call(:moved_to) - event_callback.call([:moved_to]) - end + it 'recognizes moved_to as moved_to' do + expect_change.call(:moved_to) + event_callback.call([:moved_to]) + end - it 'recognizes moved_from as moved_from' do - expect_change.call(:moved_from) - event_callback.call([:moved_from]) - end + it 'recognizes moved_from as moved_from' do + expect_change.call(:moved_from) + event_callback.call([:moved_from]) end end end diff --git a/spec/lib/listen/adapter/simulated_darwin_spec.rb b/spec/lib/listen/adapter/simulated_darwin_spec.rb new file mode 100644 index 00000000..d2e0bee1 --- /dev/null +++ b/spec/lib/listen/adapter/simulated_darwin_spec.rb @@ -0,0 +1,56 @@ +RSpec.describe Listen::Adapter::SimulatedDarwin::FakeEvent do + subject do + described_class.new(watched_dir, event) + end + + let(:watched_dir) { Pathname('/foo') } + let(:event) do + double('event', + name: item, + watcher: double('watcher', path: '/foo/dir'), + flags: flags + ) + end + + describe '#dir' do + context 'when a file is given' do + let(:item) { 'file.txt' } + let(:flags) { [] } + it 'is the containing dir' do + expect(subject.dir).to eq('/foo/dir/') + end + end + + context 'when a dir is given' do + let(:item) { 'dir1' } + let(:flags) { [:isdir] } + it 'is the containing dir' do + expect(subject.dir).to eq('/foo/dir/') + end + end + end + + # For debugging only + describe '#real_path' do + let(:item) { 'file.txt' } + let(:flags) { [] } + it 'is the path of the changed file relative to watched dir' do + expect(subject.real_path).to eq('dir/file.txt') + end + end +end + +RSpec.describe Listen::Adapter::SimulatedDarwin do + describe 'class' do + subject { described_class } + if linux? + if /1|true/ =~ ENV['LISTEN_GEM_SIMULATE_FSEVENT'] + it { should be_usable } + else + it { should_not be_usable } + end + else + it { should_not be_usable } + end + end +end diff --git a/spec/lib/listen/adapter_spec.rb b/spec/lib/listen/adapter_spec.rb index ff0164ac..9461cd32 100644 --- a/spec/lib/listen/adapter_spec.rb +++ b/spec/lib/listen/adapter_spec.rb @@ -4,6 +4,7 @@ before do allow(Listen::Adapter::BSD).to receive(:usable?) { false } allow(Listen::Adapter::Darwin).to receive(:usable?) { false } + allow(Listen::Adapter::SimulatedDarwin).to receive(:usable?) { false } allow(Listen::Adapter::Linux).to receive(:usable?) { false } allow(Listen::Adapter::Windows).to receive(:usable?) { false } end @@ -47,7 +48,19 @@ context "when on Linux" do before { allow(Listen::Adapter::Linux).to receive(:usable?) { true } } - it { is_expected.to be Listen::Adapter::Linux } + context "when simulation mode is on" do + before do + allow(Listen::Adapter::SimulatedDarwin).to receive(:usable?) { true } + end + it { is_expected.to be Listen::Adapter::SimulatedDarwin } + end + + context "when simulation mode is off" do + before do + allow(Listen::Adapter::SimulatedDarwin).to receive(:usable?) { false } + end + it { is_expected.to be Listen::Adapter::Linux } + end context "when TCP is requested" do let(:options) { { force_tcp: true } } From 6bbca69fc2d4d12c9ab42639902d74eb67b15f1a Mon Sep 17 00:00:00 2001 From: Cezary Baginski Date: Sat, 30 May 2015 15:41:59 +0200 Subject: [PATCH 05/10] make recursion configurable (Darwin) --- lib/listen/adapter/darwin.rb | 9 +++++- spec/lib/listen/adapter/darwin_spec.rb | 41 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/lib/listen/adapter/darwin.rb b/lib/listen/adapter/darwin.rb index bab73d2f..70757733 100644 --- a/lib/listen/adapter/darwin.rb +++ b/lib/listen/adapter/darwin.rb @@ -45,7 +45,14 @@ def _process_event(dir, event) _log :debug, "fsevent: #{new_path}" # TODO: does this preserve symlinks? rel_path = new_path.relative_path_from(dir).to_s - _queue_change(:dir, dir, rel_path, recursive: true) + + options = + if /1|true/ =~ ENV['LISTEN_GEM_FSEVENT_NO_RECURSION'] + {} + else + {recursion: true} + end + _queue_change(:dir, dir, rel_path, options) end end end diff --git a/spec/lib/listen/adapter/darwin_spec.rb b/spec/lib/listen/adapter/darwin_spec.rb index e87a2167..dcf34df5 100644 --- a/spec/lib/listen/adapter/darwin_spec.rb +++ b/spec/lib/listen/adapter/darwin_spec.rb @@ -54,6 +54,47 @@ subject.configure end + describe 'handling events' do + let(:config) { { dir1: foo1 } } + + subject do + dirs = config.keys.map { |p| Pathname(p.to_s) } + described_class.new(options.merge(mq: mq, directories: dirs)) + end + + around do |example| + old = ENV['LISTEN_GEM_FSEVENT_NO_RECURSION'] + ENV['LISTEN_GEM_FSEVENT_NO_RECURSION'] = env_value + example.run + ENV['LISTEN_GEM_FSEVENT_NO_RECURSION'] = old + end + + before do + allow(mq).to receive(:send) + callbacks = subject.instance_variable_get(:@callbacks) + dir = callbacks.keys.first + ev = ["#{dir.to_s}/foo/bar/baz/"] + callback = callbacks[dir] + callback.call(ev) + end + + context "when special env is set" do + let(:env_value) { "1" } + + it "does not force recursion" do + expect(mq).to have_received(:send).with(:_queue_raw_change, :dir, Pathname('dir1'), 'foo/bar/baz', {}) + end + end + + context "when special env is not set" do + let(:env_value) { nil } + # TODO: switch default when works well without recursion + it "uses recursion by default" do + expect(mq).to have_received(:send).with(:_queue_raw_change, :dir, Pathname('dir1'), 'foo/bar/baz', {recursion: true}) + end + end + end + describe 'configuration' do context 'with 1 directory' do let(:config) { { dir1: foo1 } } From 1b8d1c8b4444a7e16ec554a88aeddec93dd8ba9a Mon Sep 17 00:00:00 2001 From: Cezary Baginski Date: Sat, 30 May 2015 15:40:53 +0200 Subject: [PATCH 06/10] test all combinations on Travis --- .travis.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f248fb91..9ac6c9e5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,5 +18,10 @@ os: - linux - osx env: - - LISTEN_TESTS_DEFAULT_LAG=0.8 LISTEN_TESTS_DEFAULT_TCP_LAG=1.2 + global: + - LISTEN_TESTS_DEFAULT_LAG=0.8 LISTEN_TESTS_DEFAULT_TCP_LAG=1.2 + matrix: + - LISTEN_GEM_FSEVENT_NO_RECURSION=0 LISTEN_GEM_SIMULATE_FSEVENT=0 + # try without recursion on both OSX and Linux (simulated OSX) + - LISTEN_GEM_FSEVENT_NO_RECURSION=1 LISTEN_GEM_SIMULATE_FSEVENT=1 sudo: false From 9a5ded7c796cc4589eb54ec51b849ac2e33e3f34 Mon Sep 17 00:00:00 2001 From: Cezary Baginski Date: Sat, 30 May 2015 18:58:23 +0200 Subject: [PATCH 07/10] update to RSpec 3.2 --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index a394f79b..c3f8a30a 100644 --- a/Gemfile +++ b/Gemfile @@ -16,7 +16,7 @@ group :test do gem 'celluloid', github: 'celluloid/celluloid', branch: '0-16-stable' gem 'celluloid-io', '>= 0.15.0' gem 'rake' - gem 'rspec', '~> 3.0.0rc1' + gem 'rspec', '~> 3.2' gem 'rspec-retry' gem 'coveralls' end From 174cbdbef4921e55b273d3e9706265f110780c06 Mon Sep 17 00:00:00 2001 From: Cezary Baginski Date: Sat, 30 May 2015 19:24:41 +0200 Subject: [PATCH 08/10] fix single spec on Rubinius --- lib/listen/queue_optimizer.rb | 2 +- spec/lib/listen/listener_spec.rb | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/listen/queue_optimizer.rb b/lib/listen/queue_optimizer.rb index d01377c0..0627013e 100644 --- a/lib/listen/queue_optimizer.rb +++ b/lib/listen/queue_optimizer.rb @@ -77,7 +77,7 @@ def _reinterpret_related_changes(cookies) [[:modified, to_dir, to_file]] else not_silenced = changes.reject do |type, _, _, path, _| - _silenced?(Pathname(path), type) + _silenced?(Pathname.new(path), type) end not_silenced.map do |_, change, dir, path, _| [table.fetch(change, change), dir, path] diff --git a/spec/lib/listen/listener_spec.rb b/spec/lib/listen/listener_spec.rb index c009c83c..bee2a217 100644 --- a/spec/lib/listen/listener_spec.rb +++ b/spec/lib/listen/listener_spec.rb @@ -139,6 +139,10 @@ before do current_path = instance_double(Pathname, to_s: '/project/path') allow(Pathname).to receive(:new).with(Dir.pwd).and_return(current_path) + + # value passed to silencer + foo = instance_double(Pathname, to_s: 'foo') + allow(Pathname).to receive(:new).with('foo').and_return(foo) end context 'when watched dir is the current dir' do From a35c2b2157294082cdae7eabba6dfec1d7a21704 Mon Sep 17 00:00:00 2001 From: Cezary Baginski Date: Sat, 30 May 2015 19:58:02 +0200 Subject: [PATCH 09/10] run polling acceptance tests separately --- .travis.yml | 1 + spec/acceptance/listen_spec.rb | 340 ++++++++++++++++----------------- spec/acceptance/tcp_spec.rb | 6 +- 3 files changed, 175 insertions(+), 172 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9ac6c9e5..cde8b5ed 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,4 +24,5 @@ env: - LISTEN_GEM_FSEVENT_NO_RECURSION=0 LISTEN_GEM_SIMULATE_FSEVENT=0 # try without recursion on both OSX and Linux (simulated OSX) - LISTEN_GEM_FSEVENT_NO_RECURSION=1 LISTEN_GEM_SIMULATE_FSEVENT=1 + - LISTEN_GEM_FSEVENT_NO_RECURSION=1 LISTEN_GEM_SIMULATE_FSEVENT=1 LISTEN_TESTS_POLLING=1 sudo: false diff --git a/spec/acceptance/listen_spec.rb b/spec/acceptance/listen_spec.rb index 25513c71..21734039 100644 --- a/spec/acceptance/listen_spec.rb +++ b/spec/acceptance/listen_spec.rb @@ -1,9 +1,14 @@ # encoding: UTF-8 RSpec.describe 'Listen' do + def polling_tests? + /1|true/i =~ ENV['LISTEN_TESTS_POLLING'] + end + let(:base_options) { { wait_for_delay: 0.1, latency: 0.1 } } - let(:polling_options) { {} } let(:options) { {} } + let(:polling_options) { { force_polling: polling_tests? } } + let(:all_options) { base_options.merge(polling_options).merge(options) } let(:wrapper) { setup_listener(all_options, :track_changes) } @@ -27,201 +32,194 @@ end end - [false, true].each do |polling| - context "force_polling option to #{polling}" do - let(:polling_options) { { force_polling: polling } } - - context 'with default ignore options' do - context 'with nothing in listen dir' do - - it { is_expected.to process_addition_of('file.rb') } - it { is_expected.to process_addition_of('.hidden') } - - it 'listens to multiple files addition' do - result = wrapper.listen do - change_fs(:added, 'file1.rb') - change_fs(:added, 'file2.rb') - end - - expect(result).to eq(modified: [], - added: %w(file1.rb file2.rb), - removed: []) - end - - it 'listens to file moved inside' do - touch '../file.rb' - expect(wrapper.listen do - mv '../file.rb', 'file.rb' - end).to eq(modified: [], added: ['file.rb'], removed: []) - end - end + context 'with default ignore options' do + context 'with nothing in listen dir' do + it { is_expected.to process_addition_of('file.rb') } + it { is_expected.to process_addition_of('.hidden') } - context 'existing file.rb in listen dir' do - around do |example| - change_fs(:added, 'file.rb') - example.run - end - - it { is_expected.to process_modification_of('file.rb') } - it { is_expected.to process_removal_of('file.rb') } - - it 'listens to file.rb moved out' do - expect(wrapper.listen do - mv 'file.rb', '../file.rb' - end).to eq(modified: [], added: [], removed: ['file.rb']) - end - - it 'listens to file mode change' do - prev_mode = File.stat('file.rb').mode - - result = wrapper.listen do - windows? ? `attrib +r file.rb` : chmod(0444, 'file.rb') - end - - new_mode = File.stat('file.rb').mode - no_event = result[:modified].empty? && prev_mode == new_mode - - # Check if chmod actually works or an attrib event happens, - # or expect nothing otherwise - # - # (e.g. fails for polling+vfat on Linux, but works with - # INotify+vfat because you get an event regardless if mode - # actually changes) - # - files = no_event ? [] : ['file.rb'] - - expect(result).to eq(modified: files, added: [], removed: []) - end + it 'listens to multiple files addition' do + result = wrapper.listen do + change_fs(:added, 'file1.rb') + change_fs(:added, 'file2.rb') end - context 'hidden file in listen dir' do - around do |example| - change_fs(:added, '.hidden') - example.run - end + expect(result).to eq(modified: [], + added: %w(file1.rb file2.rb), + removed: []) + end - it { is_expected.to process_modification_of('.hidden') } - end + it 'listens to file moved inside' do + touch '../file.rb' + expect(wrapper.listen do + mv '../file.rb', 'file.rb' + end).to eq(modified: [], added: ['file.rb'], removed: []) + end + end - context 'dir in listen dir' do - around do |example| - mkdir_p 'dir' - example.run - end + context 'existing file.rb in listen dir' do + around do |example| + change_fs(:added, 'file.rb') + example.run + end - it { is_expected.to process_addition_of('dir/file.rb') } - end + it { is_expected.to process_modification_of('file.rb') } + it { is_expected.to process_removal_of('file.rb') } - context 'dir with file in listen dir' do - around do |example| - mkdir_p 'dir' - touch 'dir/file.rb' - example.run - end - - it 'listens to file move' do - expected = { modified: [], - added: %w(file.rb), - removed: %w(dir/file.rb) - } - - expect(wrapper.listen do - mv 'dir/file.rb', 'file.rb' - end).to eq expected - end - end + it 'listens to file.rb moved out' do + expect(wrapper.listen do + mv 'file.rb', '../file.rb' + end).to eq(modified: [], added: [], removed: ['file.rb']) + end + + it 'listens to file mode change' do + prev_mode = File.stat('file.rb').mode - context 'two dirs with files in listen dir' do - around do |example| - mkdir_p 'dir1' - touch 'dir1/file1.rb' - mkdir_p 'dir2' - touch 'dir2/file2.rb' - example.run - end - - it 'listens to multiple file moves' do - expected = { - modified: [], - added: ['dir1/file2.rb', 'dir2/file1.rb'], - removed: ['dir1/file1.rb', 'dir2/file2.rb'] - } - - expect(wrapper.listen do - mv 'dir1/file1.rb', 'dir2/file1.rb' - mv 'dir2/file2.rb', 'dir1/file2.rb' - end).to eq expected - end - - it 'listens to dir move' do - expected = { modified: [], - added: ['dir2/dir1/file1.rb'], - removed: ['dir1/file1.rb'] } - - expect(wrapper.listen do - mv 'dir1', 'dir2/' - end).to eq expected - end + result = wrapper.listen do + windows? ? `attrib +r file.rb` : chmod(0444, 'file.rb') end - context 'with .bundle dir ignored by default' do - around do |example| - mkdir_p '.bundle' - example.run - end + new_mode = File.stat('file.rb').mode + no_event = result[:modified].empty? && prev_mode == new_mode - it { is_expected.not_to process_addition_of('.bundle/file.rb') } - end + # Check if chmod actually works or an attrib event happens, + # or expect nothing otherwise + # + # (e.g. fails for polling+vfat on Linux, but works with + # INotify+vfat because you get an event regardless if mode + # actually changes) + # + files = no_event ? [] : ['file.rb'] + + expect(result).to eq(modified: files, added: [], removed: []) end + end - context 'when :ignore is *ignored_dir*' do - context 'ignored dir with file in listen dir' do - let(:options) { { ignore: /ignored_dir/ } } + context 'hidden file in listen dir' do + around do |example| + change_fs(:added, '.hidden') + example.run + end - around do |example| - mkdir_p 'ignored_dir' - example.run - end + it { is_expected.to process_modification_of('.hidden') } + end - it { is_expected.not_to process_addition_of('ignored_dir/file.rb') } - end + context 'dir in listen dir' do + around do |example| + mkdir_p 'dir' + example.run + end - context 'when :only is *.rb' do - let(:options) { { only: /\.rb$/ } } + it { is_expected.to process_addition_of('dir/file.rb') } + end - it { is_expected.to process_addition_of('file.rb') } - it { is_expected.not_to process_addition_of('file.txt') } - end + context 'dir with file in listen dir' do + around do |example| + mkdir_p 'dir' + touch 'dir/file.rb' + example.run + end - context 'when :ignore is bar.rb' do - context 'when :only is *.rb' do - let(:options) { { ignore: /bar\.rb$/, only: /\.rb$/ } } + it 'listens to file move' do + expected = { modified: [], + added: %w(file.rb), + removed: %w(dir/file.rb) + } - it { is_expected.to process_addition_of('file.rb') } - it { is_expected.not_to process_addition_of('file.txt') } - it { is_expected.not_to process_addition_of('bar.rb') } - end - end + expect(wrapper.listen do + mv 'dir/file.rb', 'file.rb' + end).to eq expected + end + end - context 'when default ignore is *.rb' do - let(:options) { { ignore: /\.rb$/ } } + context 'two dirs with files in listen dir' do + around do |example| + mkdir_p 'dir1' + touch 'dir1/file1.rb' + mkdir_p 'dir2' + touch 'dir2/file2.rb' + example.run + end - it { is_expected.not_to process_addition_of('file.rb') } + it 'listens to multiple file moves' do + expected = { + modified: [], + added: ['dir1/file2.rb', 'dir2/file1.rb'], + removed: ['dir1/file1.rb', 'dir2/file2.rb'] + } + + expect(wrapper.listen do + mv 'dir1/file1.rb', 'dir2/file1.rb' + mv 'dir2/file2.rb', 'dir1/file2.rb' + end).to eq expected + end - context 'with #ignore on *.txt mask' do - before { wrapper.listener.ignore(/\.txt/) } + it 'listens to dir move' do + expected = { modified: [], + added: ['dir2/dir1/file1.rb'], + removed: ['dir1/file1.rb'] } - it { is_expected.not_to process_addition_of('file.rb') } - it { is_expected.not_to process_addition_of('file.txt') } - end + expect(wrapper.listen do + mv 'dir1', 'dir2/' + end).to eq expected + end + end - context 'with #ignore! on *.txt mask' do - before { wrapper.listener.ignore!(/\.txt/) } + context 'with .bundle dir ignored by default' do + around do |example| + mkdir_p '.bundle' + example.run + end - it { is_expected.to process_addition_of('file.rb') } - it { is_expected.not_to process_addition_of('file.txt') } - end - end + it { is_expected.not_to process_addition_of('.bundle/file.rb') } + end + end + + context 'when :ignore is *ignored_dir*' do + context 'ignored dir with file in listen dir' do + let(:options) { { ignore: /ignored_dir/ } } + + around do |example| + mkdir_p 'ignored_dir' + example.run + end + + it { is_expected.not_to process_addition_of('ignored_dir/file.rb') } + end + + context 'when :only is *.rb' do + let(:options) { { only: /\.rb$/ } } + + it { is_expected.to process_addition_of('file.rb') } + it { is_expected.not_to process_addition_of('file.txt') } + end + + context 'when :ignore is bar.rb' do + context 'when :only is *.rb' do + let(:options) { { ignore: /bar\.rb$/, only: /\.rb$/ } } + + it { is_expected.to process_addition_of('file.rb') } + it { is_expected.not_to process_addition_of('file.txt') } + it { is_expected.not_to process_addition_of('bar.rb') } + end + end + + context 'when default ignore is *.rb' do + let(:options) { { ignore: /\.rb$/ } } + + it { is_expected.not_to process_addition_of('file.rb') } + + context 'with #ignore on *.txt mask' do + before { wrapper.listener.ignore(/\.txt/) } + + it { is_expected.not_to process_addition_of('file.rb') } + it { is_expected.not_to process_addition_of('file.txt') } + end + + context 'with #ignore! on *.txt mask' do + before { wrapper.listener.ignore!(/\.txt/) } + + it { is_expected.to process_addition_of('file.rb') } + it { is_expected.not_to process_addition_of('file.txt') } end end end diff --git a/spec/acceptance/tcp_spec.rb b/spec/acceptance/tcp_spec.rb index d2551fc4..f5427652 100644 --- a/spec/acceptance/tcp_spec.rb +++ b/spec/acceptance/tcp_spec.rb @@ -1,7 +1,11 @@ RSpec.describe Listen::Listener do + def polling_tests? + /1|true/i =~ ENV['LISTEN_TESTS_POLLING'] + end let(:port) { 4000 } - let(:broadcast_options) { { forward_to: port } } + let(:polling_options) { { force_polling: polling_tests? } } + let(:broadcast_options) { { forward_to: port }.merge(polling_options) } let(:paths) { Pathname.new(Dir.pwd) } around do |example| From 67f50e757dd09921029959bb2179c5af6511e98f Mon Sep 17 00:00:00 2001 From: Cezary Baginski Date: Sat, 30 May 2015 20:30:31 +0200 Subject: [PATCH 10/10] show warning when recursion is off (Darwin) --- lib/listen/adapter/darwin.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/listen/adapter/darwin.rb b/lib/listen/adapter/darwin.rb index 70757733..973d0934 100644 --- a/lib/listen/adapter/darwin.rb +++ b/lib/listen/adapter/darwin.rb @@ -14,6 +14,10 @@ class Darwin < Base # NOTE: each directory gets a DIFFERENT callback! def _configure(dir, &callback) + if /1|true/ =~ ENV['LISTEN_GEM_FSEVENT_NO_RECURSION'] + STDERR.puts "WARNING: Recursive scanning is disabled, which should"\ + " be faster, but not all changes may be properly detected yet." + end require 'rb-fsevent' opts = { latency: options.latency }