diff --git a/lib/listen/thread.rb b/lib/listen/thread.rb index 2143580e..a1e6b3b6 100644 --- a/lib/listen/thread.rb +++ b/lib/listen/thread.rb @@ -24,7 +24,7 @@ def new(name, &block) def rescue_and_log(method_name, *args, caller_stack: nil) yield(*args) - rescue Exception => exception # rubocop:disable Lint/RescueException + rescue => exception _log_exception(exception, method_name, caller_stack: caller_stack) end diff --git a/listen.gemspec b/listen.gemspec index 19106787..ed1ec0ce 100644 --- a/listen.gemspec +++ b/listen.gemspec @@ -5,7 +5,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'listen/version' -Gem::Specification.new do |gem| +Gem::Specification.new do |gem| # rubocop:disable Metrics/BlockLength gem.name = 'listen' gem.version = Listen::VERSION gem.license = 'MIT' diff --git a/spec/lib/listen/adapter/linux_spec.rb b/spec/lib/listen/adapter/linux_spec.rb index e7981b1a..9d9caff6 100644 --- a/spec/lib/listen/adapter/linux_spec.rb +++ b/spec/lib/listen/adapter/linux_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.describe Listen::Adapter::Linux do - describe 'class' do + describe 'class methods' do subject { described_class } if linux? @@ -12,177 +12,185 @@ end if linux? - before(:all) do - require 'rb-inotify' - end - let(:dir1) { Pathname.new("/foo/dir1") } - - let(:config) { instance_double(Listen::Adapter::Config, "config") } - let(:queue) { instance_double(Queue, "queue") } - let(:silencer) { instance_double(Listen::Silencer, "silencer") } - let(:snapshot) { instance_double(Listen::Change, "snapshot") } - let(:record) { instance_double(Listen::Record, "record") } - - # TODO: fix other adapters too! - subject { described_class.new(config) } - - describe 'watch events' do - let(:directories) { [Pathname.pwd] } - let(:adapter_options) { {} } - let(:default_events) { [:recursive, :attrib, :create, :modify, :delete, :move, :close_write] } - let(:fake_worker) { double(:fake_worker) } - let(:fake_notifier) { double(:fake_notifier, new: fake_worker) } - - before do - stub_const('INotify::Notifier', fake_notifier) - - allow(config).to receive(:directories).and_return(directories) - allow(config).to receive(:adapter_options).and_return(adapter_options) - allow(config).to receive(:queue).and_return(queue) - allow(config).to receive(:silencer).and_return(silencer) - end - - it 'starts by calling watch with default events' do - expect(fake_worker).to receive(:watch).with(*directories.map(&:to_s), *default_events) - subject.start + describe 'instance methods' do + before(:all) do + require 'rb-inotify' end - end - describe 'inotify limit message' do - let(:directories) { [Pathname.pwd] } - let(:adapter_options) { {} } + let(:dir1) { Pathname.new("/foo/dir1") } - before do - fake_worker = double(:fake_worker) - allow(fake_worker).to receive(:watch).and_raise(Errno::ENOSPC) + let(:queue) { instance_double(Queue, "queue", close: nil) } + let(:config) { instance_double(Listen::Adapter::Config, "config", queue: queue) } + let(:silencer) { instance_double(Listen::Silencer, "silencer") } + let(:snapshot) { instance_double(Listen::Change, "snapshot") } + let(:record) { instance_double(Listen::Record, "record") } - fake_notifier = double(:fake_notifier, new: fake_worker) - stub_const('INotify::Notifier', fake_notifier) + # TODO: fix other adapters too! + subject { described_class.new(config) } - allow(config).to receive(:directories).and_return(directories) - allow(config).to receive(:adapter_options).and_return(adapter_options) + after do + subject.stop end - it 'should be shown before calling abort' do - expected_message = described_class.const_get('INOTIFY_LIMIT_MESSAGE') - expect { subject.start }.to raise_error SystemExit, expected_message - end - end - - # TODO: should probably be adapted to be more like adapter/base_spec.rb - describe '_callback' do - let(:directories) { [dir1] } - let(:adapter_options) { { events: [:recursive, :close_write] } } + describe 'watch events' do + let(:directories) { [Pathname.pwd] } + let(:adapter_options) { {} } + let(:default_events) { [:recursive, :attrib, :create, :modify, :delete, :move, :close_write] } + let(:fake_worker) { double(:fake_worker_for_watch_events) } + let(:fake_notifier) { double(:fake_notifier, new: fake_worker) } - before do - fake_worker = double(:fake_worker) - events = [:recursive, :close_write] - allow(fake_worker).to receive(:watch).with('/foo/dir1', *events) - - fake_notifier = double(:fake_notifier, new: fake_worker) - stub_const('INotify::Notifier', fake_notifier) - - allow(config).to receive(:directories).and_return(directories) - allow(config).to receive(:adapter_options).and_return(adapter_options) - allow(config).to receive(:queue).and_return(queue) - allow(config).to receive(:silencer).and_return(silencer) - - allow(Listen::Record).to receive(:new).with(dir1).and_return(record) - allow(Listen::Change::Config).to receive(:new).with(queue, silencer). - and_return(config) - allow(Listen::Change).to receive(:new).with(config, record). - and_return(snapshot) + before do + stub_const('INotify::Notifier', fake_notifier) - allow(subject).to receive(:require).with('rb-inotify') - subject.configure - end + allow(config).to receive(:directories).and_return(directories) + allow(config).to receive(:adapter_options).and_return(adapter_options) + allow(config).to receive(:silencer).and_return(silencer) + allow(fake_worker).to receive(:close) + end - let(:expect_change) do - lambda do |change| - expect(snapshot).to receive(:invalidate).with( - :file, - 'path/foo.txt', - cookie: 123, - change: change - ) + after do + subject.stop end - end - let(:event_callback) do - lambda do |flags| - callbacks = subject.instance_variable_get(:'@callbacks') - callbacks.values.flatten.each do |callback| - callback.call double( - :inotify_event, - name: 'foo.txt', - watcher: double(:watcher, path: '/foo/dir1/path'), - flags: flags, - cookie: 123) - end + it 'starts by calling watch with default events' do + expect(fake_worker).to receive(:watch).with(*directories.map(&:to_s), *default_events) + subject.start end 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 + describe 'inotify limit message' do + let(:directories) { [Pathname.pwd] } + let(:adapter_options) { {} } - it 'recognizes moved_to as moved_to' do - expect_change.call(:moved_to) - event_callback.call([:moved_to]) + before do + fake_worker = double(:fake_worker_for_inotify_limit_message) + allow(fake_worker).to receive(:watch).and_raise(Errno::ENOSPC) + allow(fake_worker).to receive(:close) + + fake_notifier = double(:fake_notifier, new: fake_worker) + stub_const('INotify::Notifier', fake_notifier) + + allow(config).to receive(:directories).and_return(directories) + allow(config).to receive(:adapter_options).and_return(adapter_options) end - it 'recognizes moved_from as moved_from' do - expect_change.call(:moved_from) - event_callback.call([:moved_from]) + it 'should be shown before calling abort' do + expected_message = described_class.const_get('INOTIFY_LIMIT_MESSAGE') + expect { subject.start }.to raise_error SystemExit, expected_message end end - end - - describe '#stop' do - let(:fake_worker) { double(:fake_worker, close: true) } - let(:directories) { [dir1] } - let(:adapter_options) { { events: [:recursive, :close_write] } } - before do - allow(config).to receive(:directories).and_return(directories) - allow(config).to receive(:adapter_options).and_return(adapter_options) - end + # TODO: should probably be adapted to be more like adapter/base_spec.rb + describe '_callback' do + let(:directories) { [dir1] } + let(:adapter_options) { { events: [:recursive, :close_write] } } - context 'when configured' do before do + fake_worker = double(:fake_worker_for_callback) events = [:recursive, :close_write] allow(fake_worker).to receive(:watch).with('/foo/dir1', *events) + allow(fake_worker).to receive(:close) fake_notifier = double(:fake_notifier, new: fake_worker) stub_const('INotify::Notifier', fake_notifier) - allow(config).to receive(:queue).and_return(queue) - allow(queue).to receive(:close) + allow(config).to receive(:directories).and_return(directories) + allow(config).to receive(:adapter_options).and_return(adapter_options) allow(config).to receive(:silencer).and_return(silencer) + allow(Listen::Record).to receive(:new).with(dir1).and_return(record) + allow(Listen::Change::Config).to receive(:new).with(queue, silencer). + and_return(config) + allow(Listen::Change).to receive(:new).with(config, record). + and_return(snapshot) + allow(subject).to receive(:require).with('rb-inotify') subject.configure end - it 'stops the worker' do - expect(fake_worker).to receive(:close) - subject.stop + let(:expect_change) do + lambda do |change| + expect(snapshot).to receive(:invalidate).with( + :file, + 'path/foo.txt', + cookie: 123, + change: change + ) + end + end + + let(:event_callback) do + lambda do |flags| + callbacks = subject.instance_variable_get(:'@callbacks') + callbacks.values.flatten.each do |callback| + callback.call double( + :inotify_event, + name: 'foo.txt', + watcher: double(:watcher, path: '/foo/dir1/path'), + flags: flags, + cookie: 123) + end + end + 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 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 end end - context 'when not even initialized' do + describe '#stop' do + let(:fake_worker) { double(:fake_worker_for_stop, close: true) } + let(:directories) { [dir1] } + let(:adapter_options) { { events: [:recursive, :close_write] } } + before do - allow(config).to receive(:queue).and_return(queue) - allow(queue).to receive(:close) + allow(config).to receive(:directories).and_return(directories) + allow(config).to receive(:adapter_options).and_return(adapter_options) end - it 'does not crash' do - expect do + context 'when configured' do + before do + events = [:recursive, :close_write] + allow(fake_worker).to receive(:watch).with('/foo/dir1', *events) + + fake_notifier = double(:fake_notifier, new: fake_worker) + stub_const('INotify::Notifier', fake_notifier) + + allow(config).to receive(:silencer).and_return(silencer) + + allow(subject).to receive(:require).with('rb-inotify') + subject.configure + end + + it 'stops the worker' do subject.stop - end.to_not raise_error + end + end + + context 'when not even initialized' do + before do + allow(queue).to receive(:close) + end + + it 'does not crash' do + expect do + subject.stop + end.to_not raise_error + end end end end diff --git a/spec/lib/listen/event/processor_spec.rb b/spec/lib/listen/event/processor_spec.rb index f3c6e6f4..e8105f3e 100644 --- a/spec/lib/listen/event/processor_spec.rb +++ b/spec/lib/listen/event/processor_spec.rb @@ -224,7 +224,7 @@ def status_for_time(time) end describe '_process_changes' do - context 'when it raises an exception derived from StandardError or not' do + context 'when it raises an exception derived from StandardError' do before do allow(event_queue).to receive(:empty?).and_return(true) allow(config).to receive(:callable?).and_return(true) @@ -232,12 +232,12 @@ def status_for_time(time) allow(config).to receive(:optimize_changes).with(anything).and_return(resulting_changes) expect(config).to receive(:call).and_raise(ArgumentError, "bang!") expect(config).to receive(:call).and_return(nil) - expect(config).to receive(:call).and_raise(ScriptError, "ruby typo!") + expect(config).to receive(:call).and_raise("error!") end it 'rescues and logs exception and continues' do expect(Listen.logger).to receive(:error).with(/Exception rescued in _process_changes:\nArgumentError: bang!/) - expect(Listen.logger).to receive(:error).with(/Exception rescued in _process_changes:\nScriptError: ruby typo!/) + expect(Listen.logger).to receive(:error).with(/Exception rescued in _process_changes:\nRuntimeError: error!/) expect(Listen.logger).to receive(:debug).with(/Callback \(exception\) took/) expect(Listen.logger).to receive(:debug).with(/Callback took/) expect(Listen.logger).to receive(:debug).with(/Callback \(exception\) took/) diff --git a/spec/lib/listen/thread_spec.rb b/spec/lib/listen/thread_spec.rb index a6966402..bd10f423 100644 --- a/spec/lib/listen/thread_spec.rb +++ b/spec/lib/listen/thread_spec.rb @@ -63,6 +63,32 @@ end end + class TestExceptionDerivedFromException < Exception; end # rubocop:disable Lint/InheritException + + context "when exception raised that is not derived from StandardError" do + [SystemExit, SystemStackError, NoMemoryError, SecurityError, TestExceptionDerivedFromException].each do |exception| + context exception.name do + let(:block) do + -> { raise exception, 'boom!' } + end + + it "does not rescue" do + expect(Thread).to receive(:new) do |&block| + expect do + block.call + end.to raise_exception(exception, 'boom!') + + thread = instance_double(Thread, "thread") + allow(thread).to receive(:name=).with(any_args) + thread + end + + subject + end + end + end + end + context "when nested exceptions raised" do let(:block) { raise_nested_exception_block } @@ -78,15 +104,6 @@ subject.join end end - - context 'when exception raised that is not derived from StandardError' do - let(:block) { raise_script_error_block } - - it "still rescues and logs" do - expect(Listen.logger).to receive(:error).with(/Exception rescued in listen-worker_thread:\nScriptError: ruby typo!/) - subject.join - end - end end describe '.rescue_and_log' do @@ -106,9 +123,10 @@ context 'when exception raised that is not derived from StandardError' do let(:block) { raise_script_error_block } - it 'still rescues and logs' do - expect(Listen.logger).to receive(:error).with(/Exception rescued in method:\nScriptError: ruby typo!/) - described_class.rescue_and_log("method", &block) + it "raises out" do + expect do + described_class.rescue_and_log("method", &raise_script_error_block) + end.to raise_exception(ScriptError, "ruby typo!") end end end