diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7415d3ce..7e5b197c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Contribute to Listen File an issue ------------- -If you haven't already, first see [TROUBLESHOOTING](https://github.com/guard/listen/wiki/Troubleshooting) for known issues, solutions and workarounds. +If you haven't already, first see [TROUBLESHOOTING](https://github.com/guard/listen/blob/master/README.md#Issues-and-Troubleshooting) for known issues, solutions and workarounds. You can report bugs and feature requests to [GitHub Issues](https://github.com/guard/listen/issues). @@ -16,7 +16,7 @@ Try to figure out where the issue belongs to: Is it an issue with Listen itself **It's most likely that your bug gets resolved faster if you provide as much information as possible!** -The MOST useful information is debugging output from Listen (`LISTEN_GEM_DEBUGGING=1`) - see [TROUBLESHOOTING](https://github.com/guard/listen/wiki/Troubleshooting) for details. +The MOST useful information is debugging output from Listen (`LISTEN_GEM_DEBUGGING=1`) - see [TROUBLESHOOTING](https://github.com/guard/listen/blob/master/README.md#Issues-and-Troubleshooting) for details. Development diff --git a/README.md b/README.md index 43d29e39..340b7c37 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ The `listen` gem listens to file modifications and notifies you about the changes. -:exclamation: `Listen` is currently accepting more maintainers. Please [read this](https://github.com/guard/guard/wiki/Maintainers) if you're interested in joining the team. - [![Development Status](https://github.com/guard/listen/workflows/Development/badge.svg)](https://github.com/guard/listen/actions?workflow=Development) [![Gem Version](https://badge.fury.io/rb/listen.svg)](http://badge.fury.io/rb/listen) [![Code Climate](https://codeclimate.com/github/guard/listen.svg)](https://codeclimate.com/github/guard/listen) diff --git a/lib/listen/adapter/linux.rb b/lib/listen/adapter/linux.rb index 50653d6e..af732491 100644 --- a/lib/listen/adapter/linux.rb +++ b/lib/listen/adapter/linux.rb @@ -21,12 +21,12 @@ class Linux < Base private - WIKI_URL = 'https://github.com/guard/listen'\ + README_URL = 'https://github.com/guard/listen'\ '/blob/master/README.md#increasing-the-amount-of-inotify-watchers' - INOTIFY_LIMIT_MESSAGE = <<-EOS.gsub(/^\s*/, '') + INOTIFY_LIMIT_MESSAGE = <<-EOS FATAL: Listen error: unable to monitor directories for changes. - Visit #{WIKI_URL} for info on how to fix this. + Visit #{README_URL} for info on how to fix this. EOS def _configure(directory, &callback) diff --git a/lib/listen/error.rb b/lib/listen/error.rb new file mode 100644 index 00000000..e3c3a27c --- /dev/null +++ b/lib/listen/error.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# Besides programming error exceptions like ArgumentError, +# all public interface exceptions should be declared here and inherit from Listen::Error. +module Listen + class Error < RuntimeError + class NotStarted < Error; end + class SymlinkLoop < Error; end + end +end diff --git a/lib/listen/event/loop.rb b/lib/listen/event/loop.rb index 9b6e2ebf..dd071f01 100644 --- a/lib/listen/event/loop.rb +++ b/lib/listen/event/loop.rb @@ -5,15 +5,15 @@ require 'timeout' require 'listen/event/processor' require 'listen/thread' +require 'listen/error' module Listen module Event class Loop include Listen::FSM - class Error < RuntimeError - class NotStarted < Error; end - end + Error = ::Listen::Error + NotStarted = ::Listen::Error::NotStarted # for backward compatibility start_state :pre_start state :pre_start @@ -40,6 +40,7 @@ def started? MAX_STARTUP_SECONDS = 5.0 + # @raises Error::NotStarted if background thread hasn't started in MAX_STARTUP_SECONDS def start # TODO: use a Fiber instead? return unless state == :pre_start diff --git a/lib/listen/fsm.rb b/lib/listen/fsm.rb index ba63fd65..154ad2b3 100644 --- a/lib/listen/fsm.rb +++ b/lib/listen/fsm.rb @@ -53,6 +53,9 @@ def initialize_fsm # if not already, waits for a state change (up to timeout seconds--`nil` means infinite) # returns truthy iff the transition to one of the desired state has occurred def wait_for_state(*wait_for_states, timeout: nil) + wait_for_states.each do |state| + state.is_a?(Symbol) or raise ArgumentError, "states must be symbols (got #{state.inspect})" + end @mutex.synchronize do if !wait_for_states.include?(@state) @state_changed.wait(@mutex, timeout) diff --git a/lib/listen/record/symlink_detector.rb b/lib/listen/record/symlink_detector.rb index c5d66ec0..cce87739 100644 --- a/lib/listen/record/symlink_detector.rb +++ b/lib/listen/record/symlink_detector.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true require 'set' +require 'listen/error' module Listen # @private api class Record class SymlinkDetector - WIKI = 'https://github.com/guard/listen/wiki/Duplicate-directory-errors' + README_URL = 'https://github.com/guard/listen/blob/master/README.md' SYMLINK_LOOP_ERROR = <<-EOS ** ERROR: directory is already being watched! ** @@ -15,11 +16,10 @@ class SymlinkDetector is already being watched through: %s - MORE INFO: #{WIKI} + MORE INFO: #{README_URL} EOS - class Error < RuntimeError - end + Error = ::Listen::Error # for backward compatibility def initialize @real_dirs = Set.new @@ -27,14 +27,14 @@ def initialize def verify_unwatched!(entry) real_path = entry.real_path - @real_dirs.add?(real_path) || _fail(entry.sys_path, real_path) + @real_dirs.add?(real_path) or _fail(entry.sys_path, real_path) end private def _fail(symlinked, real_path) warn(format(SYMLINK_LOOP_ERROR, symlinked, real_path)) - raise Error, 'Failed due to looped symlinks' + raise ::Listen::Error::SymlinkLoop, 'Failed due to looped symlinks' end end end diff --git a/listen.gemspec b/listen.gemspec index 9ac4b647..19106787 100644 --- a/listen.gemspec +++ b/listen.gemspec @@ -21,8 +21,7 @@ Gem::Specification.new do |gem| 'changelog_uri' => "#{gem.homepage}/releases", 'documentation_uri' => "https://www.rubydoc.info/gems/listen/#{gem.version}", 'homepage_uri' => gem.homepage, - 'source_code_uri' => "#{gem.homepage}/tree/v#{gem.version}", - 'wiki_uri' => "#{gem.homepage}/wiki" + 'source_code_uri' => "#{gem.homepage}/tree/v#{gem.version}" } gem.files = `git ls-files -z`.split("\x0").select do |f| diff --git a/spec/lib/listen/event/loop_spec.rb b/spec/lib/listen/event/loop_spec.rb index 749d4351..1d2aed7a 100644 --- a/spec/lib/listen/event/loop_spec.rb +++ b/spec/lib/listen/event/loop_spec.rb @@ -45,26 +45,40 @@ end describe '#start' do - before do + it 'is started' do + expect(processor).to receive(:loop_for).with(1.234) expect(Thread).to receive(:new) do |&block| block.call thread end - - expect(processor).to receive(:loop_for).with(1.234) - subject.start - end - - it 'is started' do expect(subject).to be_started end context 'when start is called again' do it 'returns silently' do + expect(processor).to receive(:loop_for).with(1.234) + expect(Thread).to receive(:new) do |&block| + block.call + thread + end + subject.start expect { subject.start }.to_not raise_exception end end + + context 'when state change to :started takes longer than 5 seconds' do + before do + expect(Thread).to receive(:new) { thread } + expect_any_instance_of(::ConditionVariable).to receive(:wait) { } # return immediately + end + + it 'raises Error::NotStarted' do + expect do + subject.start + end.to raise_exception(::Listen::Error::NotStarted, "thread didn't start in 5.0 seconds (in state: :starting)") + end + end end context 'when set up / started' do diff --git a/spec/lib/listen/fsm_spec.rb b/spec/lib/listen/fsm_spec.rb index a4395f28..359afa75 100644 --- a/spec/lib/listen/fsm_spec.rb +++ b/spec/lib/listen/fsm_spec.rb @@ -60,6 +60,44 @@ def initialize expect { subject.transition(:started) }.to raise_exception(NoMethodError, /private.*transition/) expect { subject.transition!(:started) }.to raise_exception(NoMethodError, /private.*transition!/) end + + describe '#wait_for_state' do + it 'returns truthy immediately if already in the desired state' do + expect(subject.instance_variable_get(:@state_changed)).to_not receive(:wait) + result = subject.wait_for_state(:initial) + expect(result).to be_truthy + end + + it 'waits for the next state change and returns truthy if then in the desired state' do + expect(subject.instance_variable_get(:@state_changed)).to receive(:wait).with(anything, anything) do + subject.instance_variable_set(:@state, :started) + end + result = subject.wait_for_state(:started) + expect(result).to be_truthy + end + + it 'waits for the next state change and returns falsey if then not the desired state' do + expect(subject.instance_variable_get(:@state_changed)).to receive(:wait).with(anything, anything) + result = subject.wait_for_state(:started) + expect(result).to be_falsey + end + + it 'passes the timeout: down to wait, if given' do + expect(subject.instance_variable_get(:@state_changed)).to receive(:wait).with(anything, 5.0) + subject.wait_for_state(:started, timeout: 5.0) + end + + it 'passes nil (infinite) timeout: down to wait, if none given' do + expect(subject.instance_variable_get(:@state_changed)).to receive(:wait).with(anything, nil) + subject.wait_for_state(:started) + end + + it 'enforces precondition that states must be symbols' do + expect do + subject.wait_for_state(:started, 'stopped') + end.to raise_exception(ArgumentError, /states must be symbols .*got "stopped"/) + end + end end context 'FSM with no start state' do