pax_global_header00006660000000000000000000000064125541355330014520gustar00rootroot0000000000000052 comment=ff4da98846c87cc025101fae2a9636672d0899ba listen-3.0.3/000077500000000000000000000000001255413553300130215ustar00rootroot00000000000000listen-3.0.3/.gitignore000066400000000000000000000003751255413553300150160ustar00rootroot00000000000000pkg/* doc/* *.gem *.rbc .*.swp *.bak bundle .bundle .yardoc .rbx .rvmrc .vagrant Gemfile.lock spec/.fixtures coverage .ruby-version example* test.txt ## MAC OS .DS_Store .Trashes .com.apple.timemachine.supported .fseventsd Desktop DB Desktop DF .idea listen-3.0.3/.hound.yml000066400000000000000000000000621255413553300147350ustar00rootroot00000000000000ruby: enabled: true config_file: .rubocop.yml listen-3.0.3/.rspec000066400000000000000000000000651255413553300141370ustar00rootroot00000000000000--color --format documentation --require spec_helper listen-3.0.3/.rubocop.yml000066400000000000000000000005261255413553300152760ustar00rootroot00000000000000inherit_from: - vendor/hound/config/style_guides/ruby.yml - .rubocop_todo.yml # Rails cops AllCops: RunRailsCops: true # Files you want to exclude AllCops: Exclude: - db/schema.rb - Gemfile - Guardfile - Rakefile # TODO: put your overrides here: Style/StringLiterals: EnforcedStyle: single_quotes Enabled: true listen-3.0.3/.rubocop_todo.yml000066400000000000000000000015521255413553300163230ustar00rootroot00000000000000# This configuration was generated by `rubocop --auto-gen-config` # on 2014-11-11 16:35:31 +0100 using RuboCop version 0.25.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. # Offense count: 2 # Configuration parameters: CountComments. Metrics/ClassLength: Max: 223 # Offense count: 7 Metrics/CyclomaticComplexity: Max: 14 # Offense count: 27 # Configuration parameters: CountComments. Metrics/MethodLength: Max: 34 # Offense count: 5 Metrics/PerceivedComplexity: Max: 16 # Offense count: 19 Style/Documentation: Enabled: false # Offense count: 44 # Configuration parameters: EnforcedStyle, SupportedStyles. Style/DotPosition: Enabled: false listen-3.0.3/.travis.yml000066400000000000000000000004621255413553300151340ustar00rootroot00000000000000language: ruby bundler_args: --without development rvm: - 2.2.2 - jruby-head - rbx-2 matrix: allow_failures: - rvm: jruby-head - rvm: rbx-2 exclude: - rvm: jruby-head os: osx - rvm: rbx-2 os: osx os: - linux - osx env: - LISTEN_TESTS_DEFAULT_LAG=0.8 sudo: false listen-3.0.3/.yardopts000066400000000000000000000002541255413553300146700ustar00rootroot00000000000000--title 'Listen Documentation' --readme README.md --markup markdown --markup-provider redcarpet --private --protected --output-dir ./doc lib/**/*.rb - CHANGELOG.md LICENSE listen-3.0.3/CHANGELOG.md000066400000000000000000000001151255413553300146270ustar00rootroot00000000000000# Moved to [GitHub releases](https://github.com/guard/listen/releases) page. listen-3.0.3/CONTRIBUTING.md000066400000000000000000000032571255413553300152610ustar00rootroot00000000000000Contribute 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. You can report bugs and feature requests to [GitHub Issues](https://github.com/guard/listen/issues). **Please don't ask question in the issue tracker**, instead ask them in our [Google group](http://groups.google.com/group/guard-dev) or on `#guard` (irc.freenode.net). Try to figure out where the issue belongs to: Is it an issue with Listen itself or with Guard? **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. Development ----------- * Documentation hosted at [RubyDoc](http://rubydoc.info/github/guard/listen/master/frames). * Source hosted at [GitHub](https://github.com/guard/listen). Pull requests are very welcome! Please try to follow these simple rules if applicable: * Please create a topic branch for every separate change you make. * Make sure your patches are well tested. All specs run with `rake spec` must pass. * Update the [Yard](http://yardoc.org/) documentation. * Update the [README](https://github.com/guard/listen/blob/master/README.md). * Update the [CHANGELOG](https://github.com/guard/listen/blob/master/CHANGELOG.md) for noteworthy changes. * Please **do not change** the version number. For questions please join us in our [Google group](http://groups.google.com/group/guard-dev) or on `#guard` (irc.freenode.net). listen-3.0.3/Gemfile000066400000000000000000000016621255413553300143210ustar00rootroot00000000000000source 'https://rubygems.org' # Create this file to use pristine/installed version of Listen for development use_installed = "./use_installed_guard" if File.exist?(use_installed) STDERR.puts "WARNING: using installed version of Listen for development" \ " (remove #{use_installed} file to use local version)" else gemspec development_group: :gem_build_tools end require 'rbconfig' case RbConfig::CONFIG['target_os'] when /mswin|mingw|cygwin/i gem 'wdm', '>= 0.1.0' when /bsd|dragonfly/i gem 'rb-kqueue', '>= 0.2' end group :test do gem 'rake' gem 'rspec', '~> 3.3' gem 'coveralls' end group :development do gem 'yard', require: false gem 'guard-rspec', require: false gem 'rubocop', '0.25.0' # TODO: should match Gemfile HoundCi gem 'guard-rubocop' gem 'pry-rescue' gem 'pry-stack_explorer', platforms: [:mri, :rbx] gem 'gems', require: false gem 'netrc', require: false gem 'octokit', require: false end listen-3.0.3/Guardfile000066400000000000000000000020501255413553300146430ustar00rootroot00000000000000ignore(%r{spec/\.fixtures/}) group :specs, halt_on_fail: true do guard :rspec, cmd: 'bundle exec rspec -t ~acceptance', failed_mode: :keep, all_after_pass: true do watch(%r{^spec/lib/.+_spec\.rb$}) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } watch(%r{^spec/support/*}) { 'spec' } watch('spec/spec_helper.rb') { 'spec' } end guard :rubocop, all_on_start: false, cli: '--rails' do watch(%r{.+\.rb$}) { |m| m[0] } watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) } watch(%r{(?:.+/)?\.rubocop_todo\.yml$}) { |m| File.dirname(m[0]) } end # TODO: guard rspec should have a configurable file for this to work # TODO: also split up Rakefile guard :rspec, cmd: 'bundle exec rspec -t acceptance', failed_mode: :keep, all_after_pass: true do watch(%r{^spec/lib/.+_spec\.rb$}) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } watch(%r{^spec/support/*}) { 'spec' } watch('spec/spec_helper.rb') { 'spec' } watch(%r{^spec/acceptance/.+_spec\.rb$}) end end listen-3.0.3/LICENSE.txt000066400000000000000000000020711255413553300146440ustar00rootroot00000000000000Copyright (c) 2013 Thibaud Guillaume-Gentil MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. listen-3.0.3/README.md000066400000000000000000000310131255413553300142760ustar00rootroot00000000000000### :warning: Listen is [looking for new maintainers](https://groups.google.com/forum/#!topic/guard-dev/2Td0QTvTIsE). Please [contact me](mailto:thibaud@thibaud.gg) if you're interested. # Listen [![Gem Version](https://badge.fury.io/rb/listen.png)](http://badge.fury.io/rb/listen) [![Build Status](https://travis-ci.org/guard/listen.png)](https://travis-ci.org/guard/listen) [![Dependency Status](https://gemnasium.com/guard/listen.png)](https://gemnasium.com/guard/listen) [![Code Climate](https://codeclimate.com/github/guard/listen.png)](https://codeclimate.com/github/guard/listen) [![Coverage Status](https://coveralls.io/repos/guard/listen/badge.png?branch=master)](https://coveralls.io/r/guard/listen) The Listen gem listens to file modifications and notifies you about the changes. ## Known issues / Quickfixes / Workarounds *NOTE: TCP functionality has been removed from Listen 3.x - please use Listen 2.x until alternative server and client gems are created/released for 3.x.* *NOTE: Ruby 1.9.3 is no longer maintained (and may not work with Listen) - it's best to upgrade to Ruby 2.2.2* For other issues, just head over here: https://github.com/guard/listen/wiki/Quickfixes,-known-issues-and-workarounds ## Tips and Techniques Make sure you know these few basic tricks: https://github.com/guard/listen/wiki/Tips-and-Techniques ## Features * OS-optimized adapters on MRI for Mac OS X 10.6+, Linux, \*BSD and Windows, [more info](#listen-adapters) below. * Detects file modification, addition and removal. * You can watch multiple directories. * Regexp-patterns for ignoring paths for more accuracy and speed * Increased change detection accuracy on OS X HFS and VFAT volumes. * Tested on MRI Ruby environments (2.0+ only) via [Travis CI](https://travis-ci.org/guard/listen), NOTE: TCP functionality has been moved to a separate gem (listen-server and listen-client) NOTES: - Some filesystems won't work without polling (VM/Vagrant Shared folders, NFS, Samba, sshfs, etc.) - Specs suite on JRuby and Rubinius aren't reliable on Travis CI, but should work. - Windows and \*BSD adapter aren't continuously and automaticaly tested. ## Pending features / issues * symlinked directories aren't fully transparent yet: https://github.com/guard/listen/issues/279 * Directory/adapter specific configuration options * Support for plugins Pull request or help is very welcome for these. ## Install The simplest way to install Listen is to use [Bundler](http://bundler.io). ```ruby gem 'listen', '~> 3.0' # NOTE: for TCP functionality, use '~> 2.10' for now ``` ## Usage Call `Listen.to` with either a single directory or multiple directories, then define the "changes" callback in a block. ``` ruby listener = Listen.to('dir/to/listen', 'dir/to/listen2') do |modified, added, removed| puts "modified absolute path: #{modified}" puts "added absolute path: #{added}" puts "removed absolute path: #{removed}" end listener.start # not blocking sleep ``` ### Pause / unpause / stop Listeners can also be easily paused/unpaused: ``` ruby listener = Listen.to('dir/path/to/listen') { |modified, added, removed| # ... } listener.start listener.paused? # => false listener.processing? # => true listener.pause # stops processing changes (but keeps on collecting them) listener.paused? # => true listener.processing? # => false listener.unpause # resumes processing changes ("start" would do the same) listener.stop # stop both listening to changes and processing them ``` Note: While paused, Listen keeps on collecting changes in the background - to clear them, call "stop" Note: You should keep track of all started listeners and stop them properly on finish. ### Ignore / ignore! Listen ignores some directories and extensions by default (See DEFAULT_IGNORED_DIRECTORIES and DEFAULT_IGNORED_EXTENSIONS in Listen::Silencer), you can add ignoring patterns with the `ignore` option/method or overwrite default with `ignore!` option/method. ``` ruby listener = Listen.to('dir/path/to/listen', ignore: /\.txt/) { |modified, added, removed| # ... } listener.start listener.ignore! /\.pkg/ # overwrite all patterns and only ignore pkg extension. listener.ignore /\.rb/ # ignore rb extension in addition of pkg. sleep ``` Note: Ignoring regexp patterns are evaluated against relative paths. Note: ignoring paths does not improve performance - except when Polling ### Only Listen catches all files (less the ignored once) by default, if you want to only listen to a specific type of file (ie: just rb extension) you should use the `only` option/method. ``` ruby listener = Listen.to('dir/path/to/listen', only: /\.rb$/) { |modified, added, removed| # ... } listener.start listener.only /_spec\.rb$/ # overwrite all existing only patterns. sleep ``` Note: ':only' regexp patterns are evaluated only against relative **file** paths. ## Changes callback Changes to the listened-to directories gets reported back to the user in a callback. The registered callback gets invoked, when there are changes, with **three** parameters: `modified`, `added` and `removed` paths, in that particular order. Paths are always returned in their absolute form. Example: ```ruby listener = Listen.to('path/to/app') do |modified, added, removed| # This block will be called when there are changes. end listener.start sleep ``` or ... ```ruby # Create a callback callback = Proc.new do |modified, added, removed| # This proc will be called when there are changes. end listener = Listen.to('dir', &callback) listener.start sleep ``` ## Options All the following options can be set through the `Listen.to` after the directory path(s) params. ```ruby ignore: [%r{/foo/bar}, /\.pid$/, /\.coffee$/] # Ignore a list of paths # default: See DEFAULT_IGNORED_DIRECTORIES and DEFAULT_IGNORED_EXTENSIONS in Listen::Silencer ignore!: %r{/foo/bar} # Same as ignore options, but overwrite default ignored paths. only: %r{.rb$} # Only listen to specific files # default: none latency: 0.5 # Set the delay (**in seconds**) between checking for changes # default: 0.25 sec (1.0 sec for polling) wait_for_delay: 4 # Set the delay (**in seconds**) between calls to the callback when changes exist # default: 0.10 sec force_polling: true # Force the use of the polling adapter # default: none relative: false # Whether changes should be relative to current dir or not # default: false debug: true # Enable Listen logger # default: false polling_fallback_message: 'custom message' # Set a custom polling fallback message (or disable it with false) # default: "Listen will be polling for changes. Learn more at https://github.com/guard/listen#listen-adapters." ``` Also, setting the environment variable `LISTEN_GEM_DEBUGGING=1` does the same as `debug: true` above. ## Listen adapters The Listen gem has a set of adapters to notify it when there are changes. There are 4 OS-specific adapters to support Darwin, Linux, \*BSD and Windows. These adapters are fast as they use some system-calls to implement the notifying function. There is also a polling adapter - although it's much slower than other adapters, it works on every platform/system and scenario (including network filesystems such as VM shared folders). The Darwin and Linux adapters are dependencies of the Listen gem so they work out of the box. For other adapters a specific gem will have to be added to your Gemfile, please read below. The Listen gem will choose the best adapter automatically, if present. If you want to force the use of the polling adapter, use the `:force_polling` option while initializing the listener. ### On Windows If your are on Windows, it's recommended to use the [`wdm`](https://github.com/Maher4Ever/wdm) adapter instead of polling. Please add the following to your Gemfile: ```ruby gem 'wdm', '>= 0.1.0' if Gem.win_platform? ``` ### On \*BSD If your are on \*BSD you can try to use the [`rb-kqueue`](https://github.com/mat813/rb-kqueue) adapter instead of polling. Please add the following to your Gemfile: ```ruby require 'rbconfig' if RbConfig::CONFIG['target_os'] =~ /bsd|dragonfly/i gem 'rb-kqueue', '>= 0.2' end ``` ### Getting the [polling fallback message](#options)? Please visit the [installation section of the Listen WIKI](https://github.com/guard/listen/wiki#installation) for more information and options for potential fixes. ### Issues and troubleshooting *NOTE: without providing the output after setting the `LISTEN_GEM_DEBUGGING=1` environment variable, it can be almost impossible to guess why listen is not working as expected.* See [TROUBLESHOOTING](https://github.com/guard/listen/wiki/Troubleshooting) ## Performance If Listen seems slow or unresponsive, make sure you're not using the Polling adapter (you should see a warning upon startup if you are). Also, if the directories you're watching contain many files, make sure you're: * not using Polling (ideally) * using `:ignore` and `:only` options to avoid tracking directories you don't care about (important with Polling and on MacOS) * running Listen with the `:latency` and `:wait_for_delay` options not too small or too big (depends on needs) * not watching directories with log files, database files or other frequently changing files * not using a version of Listen prior to 2.7.7 * not getting silent crashes within Listen (see LISTEN_GEM_DEBUGGING=2) * not running multiple instances of Listen in the background * using a file system with atime modification disabled (ideally) * not using a filesystem with inaccurate file modification times (ideally), e.g. HFS, VFAT * not buffering to a slow terminal (e.g. transparency + fancy font + slow gfx card + lots of output) * ideally not running a slow encryption stack, e.g. btrfs + ecryptfs When in doubt, LISTEN_GEM_DEBUGGING=2 can help discover the actual events and time they happened. ## Development * Documentation hosted at [RubyDoc](http://rubydoc.info/github/guard/listen/master/frames). * Source hosted at [GitHub](https://github.com/guard/listen). Pull requests are very welcome! Please try to follow these simple rules if applicable: * Please create a topic branch for every separate change you make. * Make sure your patches are well tested. All specs must pass on [Travis CI](https://travis-ci.org/guard/listen). * Update the [Yard](http://yardoc.org/) documentation. * Update the [README](https://github.com/guard/listen/blob/master/README.md). * Please **do not change** the version number. For questions please join us in our [Google group](http://groups.google.com/group/guard-dev) or on `#guard` (irc.freenode.net). ## Acknowledgments * [Michael Kessler (netzpirat)][] for having written the [initial specs](https://github.com/guard/listen/commit/1e457b13b1bb8a25d2240428ce5ed488bafbed1f). * [Travis Tilley (ttilley)][] for this awesome work on [fssm][] & [rb-fsevent][]. * [Nathan Weizenbaum (nex3)][] for [rb-inotify][], a thorough inotify wrapper. * [Mathieu Arnold (mat813)][] for [rb-kqueue][], a simple kqueue wrapper. * [Maher Sallam][] for [wdm][], windows support wouldn't exist without him. * [Yehuda Katz (wycats)][] for [vigilo][], that has been a great source of inspiration. ## Author [Thibaud Guillaume-Gentil](https://github.com/thibaudgg) ([@thibaudgg](https://twitter.com/thibaudgg)) ## Contributors [https://github.com/guard/listen/graphs/contributors](https://github.com/guard/listen/graphs/contributors) [Thibaud Guillaume-Gentil (thibaudgg)]: https://github.com/thibaudgg [Maher Sallam]: https://github.com/Maher4Ever [Michael Kessler (netzpirat)]: https://github.com/netzpirat [Travis Tilley (ttilley)]: https://github.com/ttilley [fssm]: https://github.com/ttilley/fssm [rb-fsevent]: https://github.com/thibaudgg/rb-fsevent [Mathieu Arnold (mat813)]: https://github.com/mat813 [Nathan Weizenbaum (nex3)]: https://github.com/nex3 [rb-inotify]: https://github.com/nex3/rb-inotify [stereobooster]: https://github.com/stereobooster [rb-fchange]: https://github.com/stereobooster/rb-fchange [rb-kqueue]: https://github.com/mat813/rb-kqueue [Yehuda Katz (wycats)]: https://github.com/wycats [vigilo]: https://github.com/wycats/vigilo [wdm]: https://github.com/Maher4Ever/wdm listen-3.0.3/Rakefile000066400000000000000000000073421255413553300144740ustar00rootroot00000000000000require 'bundler/gem_tasks' require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) if ENV["CI"] != "true" require "rubocop/rake_task" RuboCop::RakeTask.new(:rubocop) task default: [:spec, :rubocop] else task default: [:spec] end class Releaser def initialize(options = {}) @project_name = options.delete(:project_name) do fail "project_name is needed!" end @gem_name = options.delete(:gem_name) do fail "gem_name is needed!" end @github_repo = options.delete(:github_repo) do fail "github_repo is needed!" end @version = options.delete(:version) do fail "version is needed!" end end def full rubygems github end def rubygems begin STDOUT.puts "Release #{@project_name} #{@version} to RubyGems? (y/n)" input = STDIN.gets.chomp.downcase end while !%w(y n).include?(input) exit if input == "n" Rake::Task["release"].invoke end def github tag_name = "v#{@version}" require "gems" _verify_released _verify_tag_pushed require "octokit" gh_client = Octokit::Client.new(netrc: true) gh_release = _detect_gh_release(gh_client, tag_name, true) return unless gh_release STDOUT.puts "Draft release for #{tag_name}:\n" STDOUT.puts gh_release.body STDOUT.puts "\n-------------------------\n\n" _confirm_publish return unless _update_release(gh_client, gh_release, tag_name) gh_release = _detect_gh_release(gh_client, tag_name, false) _success_summary(gh_release, tag_name) end private def _verify_released latest = Gems.info(@gem_name)["version"] return if @version == latest STDOUT.puts format( "%s %s is not yet released (latest: %s)", @project_name, @version, latest.inspect ) STDOUT.puts "Please release it first with: rake release:gem" exit end def _verify_tag_pushed tags = `git ls-remote --tags origin`.split("\n") return if tags.detect { |tag| tag =~ /v#{@version}$/ } STDOUT.puts "The tag v#{@version} has not yet been pushed." STDOUT.puts "Please push it first with: rake release:gem" exit end def _success_summary(gh_release, tag_name) href = gh_release.rels[:html].href STDOUT.puts "GitHub release #{tag_name} has been published!" STDOUT.puts "\nPlease enjoy and spread the word!" STDOUT.puts "Lack of inspiration? Here's a tweet you could improve:\n\n" STDOUT.puts "Just released #{@project_name} #{@version}! #{href}" end def _detect_gh_release(gh_client, tag_name, draft) gh_releases = gh_client.releases(@github_repo) gh_releases.detect { |r| r.tag_name == tag_name && r.draft == draft } end def _confirm_publish begin STDOUT.puts "Would you like to publish this GitHub release now? (y/n)" input = STDIN.gets.chomp.downcase end while !%w(y n).include?(input) exit if input == "n" end def _update_release(gh_client, gh_release, tag_name) result = gh_client.update_release(gh_release.rels[:self].href, draft: false) return true if result STDOUT.puts "GitHub release #{tag_name} couldn't be published!" false end end PROJECT_NAME = "Listen" CURRENT_VERSION = Listen::VERSION def releaser $releaser ||= Releaser.new( project_name: PROJECT_NAME, gem_name: "listen", github_repo: "guard/listen", version: CURRENT_VERSION) end namespace :release do desc "Push #{PROJECT_NAME} #{CURRENT_VERSION} to RubyGems and publish"\ " its GitHub release" task full: ["release:gem", "release:github"] desc "Push #{PROJECT_NAME} #{CURRENT_VERSION} to RubyGems" task :gem do releaser.rubygems end desc "Publish #{PROJECT_NAME} #{CURRENT_VERSION} GitHub release" task :github do releaser.github end end listen-3.0.3/bin/000077500000000000000000000000001255413553300135715ustar00rootroot00000000000000listen-3.0.3/bin/listen000077500000000000000000000003241255413553300150140ustar00rootroot00000000000000#!/usr/bin/env ruby require 'listen' require 'listen/cli' unless defined?(JRUBY_VERSION) if Signal.list.keys.include?('INT') Signal.trap('INT') { Thread.new { Listen.stop } } end end Listen::CLI.start listen-3.0.3/lib/000077500000000000000000000000001255413553300135675ustar00rootroot00000000000000listen-3.0.3/lib/listen.rb000066400000000000000000000032141255413553300154120ustar00rootroot00000000000000require 'logger' require 'listen/logger' require 'listen/listener' require 'listen/internals/thread_pool' # Always set up logging by default first time file is required # # NOTE: If you need to clear the logger completely, do so *after* # requiring this file. If you need to set a custom logger, # require the listen/logger file and set the logger before requiring # this file. Listen.setup_default_logger_if_unset # Won't print anything by default because of level - unless you've set # LISTEN_GEM_DEBUGGING or provided your own logger with a high enough level Listen::Logger.info "Listen loglevel set to: #{Listen.logger.level}" Listen::Logger.info "Listen version: #{Listen::VERSION}" module Listen class << self # Listens to file system modifications on a either single directory or # multiple directories. # # @param (see Listen::Listener#new) # # @yield [modified, added, removed] the changed files # @yieldparam [Array] modified the list of modified files # @yieldparam [Array] added the list of added files # @yieldparam [Array] removed the list of removed files # # @return [Listen::Listener] the listener # def to(*args, &block) @listeners ||= [] Listener.new(*args, &block).tap do |listener| @listeners << listener end end # This is used by the `listen` binary to handle Ctrl-C # def stop Internals::ThreadPool.stop @listeners ||= [] # TODO: should use a mutex for this @listeners.each do |listener| # call stop to halt the main loop listener.stop end @listeners = nil end end end listen-3.0.3/lib/listen/000077500000000000000000000000001255413553300150655ustar00rootroot00000000000000listen-3.0.3/lib/listen/adapter.rb000066400000000000000000000024521255413553300170350ustar00rootroot00000000000000require 'listen/adapter/base' require 'listen/adapter/bsd' require 'listen/adapter/darwin' require 'listen/adapter/linux' require 'listen/adapter/polling' require 'listen/adapter/windows' module Listen module Adapter OPTIMIZED_ADAPTERS = [Darwin, Linux, BSD, Windows] POLLING_FALLBACK_MESSAGE = 'Listen will be polling for changes.'\ 'Learn more at https://github.com/guard/listen#listen-adapters.' def self.select(options = {}) _log :debug, 'Adapter: considering polling ...' return Polling if options[:force_polling] _log :debug, 'Adapter: considering optimized backend...' return _usable_adapter_class if _usable_adapter_class _log :debug, 'Adapter: falling back to polling...' _warn_polling_fallback(options) Polling rescue _log :warn, format('Adapter: failed: %s:%s', $ERROR_POSITION.inspect, $ERROR_POSITION * "\n") raise end private def self._usable_adapter_class OPTIMIZED_ADAPTERS.detect(&:usable?) end def self._warn_polling_fallback(options) msg = options.fetch(:polling_fallback_message, POLLING_FALLBACK_MESSAGE) Kernel.warn "[Listen warning]:\n #{msg}" if msg end def self._log(type, message) Listen::Logger.send(type, message) end end end listen-3.0.3/lib/listen/adapter/000077500000000000000000000000001255413553300165055ustar00rootroot00000000000000listen-3.0.3/lib/listen/adapter/base.rb000066400000000000000000000062671255413553300177570ustar00rootroot00000000000000require 'listen/options' require 'listen/record' require 'listen/change' module Listen module Adapter class Base attr_reader :options # TODO: only used by tests DEFAULTS = {} attr_reader :config def initialize(config) @started = false @config = config @configured = nil fail 'No directories to watch!' if config.directories.empty? defaults = self.class.const_get('DEFAULTS') @options = Listen::Options.new(config.adapter_options, defaults) rescue _log_exception 'adapter config failed: %s:%s called from: %s', caller raise end # TODO: it's a separate method as a temporary workaround for tests def configure if @configured _log(:warn, 'Adapter already configured!') return end @configured = true @callbacks ||= {} config.directories.each do |dir| callback = @callbacks[dir] || lambda do |event| _process_event(dir, event) end @callbacks[dir] = callback _configure(dir, &callback) end @snapshots ||= {} # TODO: separate config per directory (some day maybe) change_config = Change::Config.new(config.queue, config.silencer) config.directories.each do |dir| record = Record.new(dir) snapshot = Change.new(change_config, record) @snapshots[dir] = snapshot end end def started? @started end def start configure if started? _log(:warn, 'Adapter already started!') return end @started = true calling_stack = caller.dup Listen::Internals::ThreadPool.add do begin @snapshots.values.each do |snapshot| _timed('Record.build()') { snapshot.record.build } end _run rescue msg = 'run() in thread failed: %s:\n'\ ' %s\n\ncalled from:\n %s' _log_exception(msg, calling_stack) raise # for unit tests mostly end end end def self.usable? const_get('OS_REGEXP') =~ RbConfig::CONFIG['target_os'] end private def _timed(title) start = Time.now.to_f yield diff = Time.now.to_f - start Listen::Logger.info format('%s: %.05f seconds', title, diff) rescue Listen::Logger.warn "#{title} crashed: #{$ERROR_INFO.inspect}" raise end # TODO: allow backend adapters to pass specific invalidation objects # e.g. Darwin -> DirRescan, INotify -> MoveScan, etc. def _queue_change(type, dir, rel_path, options) @snapshots[dir].invalidate(type, rel_path, options) end def _log(*args, &block) self.class.send(:_log, *args, &block) end def _log_exception(msg, caller_stack) formatted = format( msg, $ERROR_INFO, $ERROR_POSITION * "\n", caller_stack * "\n" ) _log(:error, formatted) end def self._log(*args, &block) Listen::Logger.send(*args, &block) end end end end listen-3.0.3/lib/listen/adapter/bsd.rb000066400000000000000000000056461255413553300176150ustar00rootroot00000000000000# Listener implementation for BSD's `kqueue`. # @see http://www.freebsd.org/cgi/man.cgi?query=kqueue # @see https://github.com/mat813/rb-kqueue/blob/master/lib/rb-kqueue/queue.rb # module Listen module Adapter class BSD < Base OS_REGEXP = /bsd|dragonfly/i DEFAULTS = { events: [ :delete, :write, :extend, :attrib, :rename # :link, :revoke ] } BUNDLER_DECLARE_GEM = <<-EOS.gsub(/^ {6}/, '') Please add the following to your Gemfile to avoid polling for changes: require 'rbconfig' if RbConfig::CONFIG['target_os'] =~ /#{OS_REGEXP}/ gem 'rb-kqueue', '>= 0.2' end EOS def self.usable? return false unless super require 'rb-kqueue' require 'find' true rescue LoadError Kernel.warn BUNDLER_DECLARE_GEM false end private def _configure(directory, &_callback) @worker ||= KQueue::Queue.new @callback = _callback # use Record to make a snapshot of dir, so we # can detect new files _find(directory.to_s) { |path| _watch_file(path, @worker) } end def _run @worker.run end def _process_event(dir, event) full_path = _event_path(event) if full_path.directory? # Force dir content tracking to kick in, or we won't have # names of added files _queue_change(:dir, dir, '.', recursive: true) elsif full_path.exist? path = full_path.relative_path_from(dir) _queue_change(:file, dir, path.to_s, change: _change(event.flags)) end # If it is a directory, and it has a write flag, it means a # file has been added so find out which and deal with it. # No need to check for removed files, kqueue will forget them # when the vfs does. _watch_for_new_file(event) if full_path.directory? end def _change(event_flags) { modified: [:attrib, :extend], added: [:write], removed: [:rename, :delete] }.each do |change, flags| return change unless (flags & event_flags).empty? end nil end def _event_path(event) Pathname.new(event.watcher.path) end def _watch_for_new_file(event) queue = event.watcher.queue _find(_event_path(event).to_s) do |file_path| unless queue.watchers.detect { |_, v| v.path == file_path.to_s } _watch_file(file_path, queue) end end end def _watch_file(path, queue) queue.watch_file(path, *options.events, &@callback) rescue Errno::ENOENT => e _log :warn, "kqueue: watch file failed: #{e.message}" end # Quick rubocop workaround def _find(*paths, &block) Find.send(:find, *paths, &block) end end end end listen-3.0.3/lib/listen/adapter/config.rb000066400000000000000000000010351255413553300202760ustar00rootroot00000000000000require 'pathname' module Listen module Adapter class Config attr_reader :directories attr_reader :silencer attr_reader :queue attr_reader :adapter_options def initialize(directories, queue, silencer, adapter_options) # TODO: fix (flatten, array, compact?) @directories = directories.map do |directory| Pathname.new(directory.to_s).realpath end @silencer = silencer @queue = queue @adapter_options = adapter_options end end end end listen-3.0.3/lib/listen/adapter/darwin.rb000066400000000000000000000027041255413553300203210ustar00rootroot00000000000000require 'thread' require 'listen/internals/thread_pool' module Listen module Adapter # Adapter implementation for Mac OS X `FSEvents`. # class Darwin < Base OS_REGEXP = /darwin(1.+)?$/i # The default delay between checking for changes. DEFAULTS = { latency: 0.1 } private # NOTE: each directory gets a DIFFERENT callback! def _configure(dir, &callback) require 'rb-fsevent' opts = { latency: options.latency } @workers ||= ::Queue.new @workers << FSEvent.new.tap do |worker| worker.watch(dir.to_s, opts, &callback) end end # NOTE: _run is called within a thread, so run every other # worker in it's own thread def _run first = @workers.pop until @workers.empty? next_worker = @workers.pop Listen::Internals::ThreadPool.add do begin next_worker.run rescue _log_exception 'run() in extra thread(s) failed: %s: %s' end end end first.run end def _process_event(dir, event) event.each do |path| new_path = Pathname.new(path.sub(/\/$/, '')) _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) end end end end end listen-3.0.3/lib/listen/adapter/linux.rb000066400000000000000000000057031255413553300201760ustar00rootroot00000000000000module Listen module Adapter # @see https://github.com/nex3/rb-inotify class Linux < Base OS_REGEXP = /linux/i DEFAULTS = { events: [ :recursive, :attrib, :create, :delete, :move, :close_write ], wait_for_delay: 0.1 } private WIKI_URL = 'https://github.com/guard/listen'\ '/wiki/Increasing-the-amount-of-inotify-watchers' INOTIFY_LIMIT_MESSAGE = <<-EOS.gsub(/^\s*/, '') FATAL: Listen error: unable to monitor directories for changes. Visit #{WIKI_URL} for info on how to fix this. EOS def _configure(directory, &callback) Kernel.require 'rb-inotify' @worker ||= ::INotify::Notifier.new @worker.watch(directory.to_s, *options.events, &callback) rescue Errno::ENOSPC abort(INOTIFY_LIMIT_MESSAGE) end def _run @worker.run end def _process_event(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 path = Pathname.new(event.watcher.path) + event.name rel_path = path.relative_path_from(dir).to_s _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 } # Note: don't pass options to force rescanning the directory, so we can # detect moving/deleting a whole tree if _dir_event?(event) _queue_change(:dir, dir, rel_path, cookie_params) return end params = cookie_params.merge(change: _change(event.flags)) _queue_change(:file, dir, rel_path, params) end def _skip_event?(event) # Event on root directory return true if event.name == '' # INotify reports changes to files inside directories as events # on the directories themselves too. # # @see http://linux.die.net/man/7/inotify _dir_event?(event) && (event.flags & [:close, :modify]).any? end def _change(event_flags) { modified: [:attrib, :close_write], moved_to: [:moved_to], moved_from: [:moved_from], added: [:create], removed: [:delete] }.each do |change, flags| return change unless (flags & event_flags).empty? end nil end def _dir_event?(event) event.flags.include?(:isdir) end end end end listen-3.0.3/lib/listen/adapter/polling.rb000066400000000000000000000017121255413553300204770ustar00rootroot00000000000000module Listen module Adapter # Polling Adapter that works cross-platform and # has no dependencies. This is the adapter that # uses the most CPU processing power and has higher # file IO than the other implementations. # class Polling < Base OS_REGEXP = // # match every OS DEFAULTS = { latency: 1.0, wait_for_delay: 0.05 } private def _configure(_, &callback) @polling_callbacks ||= [] @polling_callbacks << callback end def _run loop do start = Time.now.to_f @polling_callbacks.each do |callback| callback.call(nil) nap_time = options.latency - (Time.now.to_f - start) # TODO: warn if nap_time is negative (polling too slow) sleep(nap_time) if nap_time > 0 end end end def _process_event(dir, _) _queue_change(:dir, dir, '.', recursive: true) end end end end listen-3.0.3/lib/listen/adapter/windows.rb000066400000000000000000000055131255413553300205300ustar00rootroot00000000000000module Listen module Adapter # Adapter implementation for Windows `wdm`. # class Windows < Base OS_REGEXP = /mswin|mingw|cygwin/i BUNDLER_DECLARE_GEM = <<-EOS.gsub(/^ {6}/, '') Please add the following to your Gemfile to avoid polling for changes: gem 'wdm', '>= 0.1.0' if Gem.win_platform? EOS def self.usable? return false unless super require 'wdm' true rescue LoadError _log :debug, format('wdm - load failed: %s:%s', $ERROR_INFO, $ERROR_POSITION * "\n") Kernel.warn BUNDLER_DECLARE_GEM false end private def _configure(dir, &callback) require 'wdm' _log :debug, 'wdm - starting...' @worker ||= WDM::Monitor.new @worker.watch_recursively(dir.to_s, :files) do |change| callback.call([:file, change]) end @worker.watch_recursively(dir.to_s, :directories) do |change| callback.call([:dir, change]) end events = [:attributes, :last_write] @worker.watch_recursively(dir.to_s, *events) do |change| callback.call([:attr, change]) end end def _run @worker.run! end def _process_event(dir, event) _log :debug, "wdm - callback: #{event.inspect}" type, change = event full_path = Pathname(change.path) rel_path = full_path.relative_path_from(dir).to_s options = { change: _change(change.type) } case type when :file _queue_change(:file, dir, rel_path, options) when :attr unless full_path.directory? _queue_change(:file, dir, rel_path, options) end when :dir if change.type == :removed # TODO: check if watched dir? _queue_change(:dir, dir, Pathname(rel_path).dirname.to_s, {}) elsif change.type == :added _queue_change(:dir, dir, rel_path, {}) else # do nothing - changed directory means either: # - removed subdirs (handled above) # - added subdirs (handled above) # - removed files (handled by _file_callback) # - added files (handled by _file_callback) # so what's left? end end rescue details = event.inspect _log :error, format('wdm - callback (%): %s:%s', details, $ERROR_INFO, $ERROR_POSITION * "\n") raise end def _change(type) { modified: [:modified, :attrib], # TODO: is attrib really passed? added: [:added, :renamed_new_file], removed: [:removed, :renamed_old_file] }.each do |change, types| return change if types.include?(type) end nil end end end end listen-3.0.3/lib/listen/backend.rb000066400000000000000000000020411255413553300167760ustar00rootroot00000000000000require 'listen/adapter' require 'listen/adapter/base' require 'listen/adapter/config' # This class just aggregates configuration object to avoid Listener specs # from exploding with huge test setup blocks module Listen class Backend def initialize(directories, queue, silencer, config) adapter_select_opts = config.adapter_select_options adapter_class = Adapter.select(adapter_select_opts) # Use default from adapter if possible @min_delay_between_events = config.min_delay_between_events @min_delay_between_events ||= adapter_class::DEFAULTS[:wait_for_delay] @min_delay_between_events ||= 0.1 adapter_opts = config.adapter_instance_options(adapter_class) aconfig = Adapter::Config.new(directories, queue, silencer, adapter_opts) @adapter = adapter_class.new(aconfig) end def start adapter.start end def stop # TODO: does nothing end def min_delay_between_events @min_delay_between_events end private attr_reader :adapter end end listen-3.0.3/lib/listen/change.rb000066400000000000000000000035561255413553300166500ustar00rootroot00000000000000require 'listen/file' require 'listen/directory' module Listen # TODO: rename to Snapshot class Change # TODO: test this class for coverage class Config def initialize(queue, silencer) @queue = queue @silencer = silencer end def silenced?(path, type) @silencer.silenced?(Pathname(path), type) end def queue(*args) @queue << args end end attr_reader :record def initialize(config, record) @config = config @record = record end # Invalidate some part of the snapshot/record (dir, file, subtree, etc.) def invalidate(type, rel_path, options) watched_dir = Pathname.new(record.root) change = options[:change] cookie = options[:cookie] if !cookie && config.silenced?(rel_path, type) Listen::Logger.debug { "(silenced): #{rel_path.inspect}" } return end path = watched_dir + rel_path Listen::Logger.debug do log_details = options[:silence] && 'recording' || change || 'unknown' "#{log_details}: #{type}:#{path} (#{options.inspect})" end if change options = cookie ? { cookie: cookie } : {} config.queue(type, change, watched_dir, rel_path, options) else if type == :dir # NOTE: POSSIBLE RECURSION # TODO: fix - use a queue instead Directory.scan(self, rel_path, options) else change = File.change(record, rel_path) return if !change || options[:silence] config.queue(:file, change, watched_dir, rel_path) end end rescue RuntimeError => ex msg = format( '%s#%s crashed %s:%s', self.class, __method__, exinspect, ex.backtrace * "\n") Listen::Logger.error(msg) raise end private attr_reader :config end end listen-3.0.3/lib/listen/cli.rb000066400000000000000000000030661255413553300161660ustar00rootroot00000000000000require 'thor' require 'listen' require 'logger' module Listen class CLI < Thor default_task :start desc 'start', 'Starts Listen' class_option :verbose, type: :boolean, default: false, aliases: '-v', banner: 'Verbose' class_option :directory, type: :array, default: '.', aliases: '-d', banner: 'The directory to listen to' class_option :relative, type: :boolean, default: false, aliases: '-r', banner: 'Convert paths relative to current directory' def start Listen::Forwarder.new(options).start end end class Forwarder attr_reader :logger def initialize(options) @options = options @logger = ::Logger.new(STDOUT) @logger.level = ::Logger::INFO @logger.formatter = proc { |_, _, _, msg| "#{msg}\n" } end def start logger.info 'Starting listen...' directory = @options[:directory] relative = @options[:relative] callback = proc do |modified, added, removed| if @options[:verbose] logger.info "+ #{added}" unless added.empty? logger.info "- #{removed}" unless removed.empty? logger.info "> #{modified}" unless modified.empty? end end listener = Listen.to( directory, relative: relative, &callback) listener.start sleep 0.5 while listener.processing? end end end listen-3.0.3/lib/listen/directory.rb000066400000000000000000000047771255413553300174350ustar00rootroot00000000000000require 'set' module Listen # TODO: refactor (turn it into a normal object, cache the stat, etc) class Directory def self.scan(snapshot, rel_path, options) record = snapshot.record dir = Pathname.new(record.root) previous = record.dir_entries(rel_path) record.add_dir(rel_path) # TODO: use children(with_directory: false) path = dir + rel_path current = Set.new(path.children) Listen::Logger.debug do format('%s: %s(%s): %s -> %s', (options[:silence] ? 'Recording' : 'Scanning'), rel_path, options.inspect, previous.inspect, current.inspect) end current.each do |full_path| type = detect_type(full_path) item_rel_path = full_path.relative_path_from(dir).to_s _change(snapshot, type, item_rel_path, options) end # TODO: this is not tested properly previous = previous.reject { |entry, _| current.include? path + entry } _async_changes(snapshot, Pathname.new(rel_path), previous, options) rescue Errno::ENOENT, Errno::EHOSTDOWN record.unset_path(rel_path) _async_changes(snapshot, Pathname.new(rel_path), previous, options) rescue Errno::ENOTDIR # TODO: path not tested record.unset_path(rel_path) _async_changes(snapshot, path, previous, options) _change(snapshot, :file, rel_path, options) rescue Listen::Logger.warn do format('scan DIED: %s:%s', $ERROR_INFO, $ERROR_POSITION * "\n") end raise end def self._async_changes(snapshot, path, previous, options) fail "Not a Pathname: #{path.inspect}" unless path.respond_to?(:children) previous.each do |entry, data| # TODO: this is a hack with insufficient testing type = data.key?(:mtime) ? :file : :dir rel_path_s = (path + entry).to_s _change(snapshot, type, rel_path_s, options) end end def self._change(snapshot, type, path, options) return snapshot.invalidate(type, path, options) if type == :dir # Minor param cleanup for tests # TODO: use a dedicated Event class opts = options.dup opts.delete(:recursive) snapshot.invalidate(type, path, opts) end def self.detect_type(full_path) # TODO: should probably check record first stat = ::File.lstat(full_path.to_s) stat.directory? ? :dir : :file rescue Errno::ENOENT # TODO: ok, it should really check the record here # report as dir for scanning :dir end end end listen-3.0.3/lib/listen/event/000077500000000000000000000000001255413553300162065ustar00rootroot00000000000000listen-3.0.3/lib/listen/event/config.rb000066400000000000000000000017671255413553300200130ustar00rootroot00000000000000module Listen module Event class Config def initialize( listener, event_queue, queue_optimizer, wait_for_delay, &block) @listener = listener @event_queue = event_queue @queue_optimizer = queue_optimizer @min_delay_between_events = wait_for_delay @block = block end def sleep(*args) Kernel.sleep(*args) end def call(*args) @block.call(*args) if @block end def timestamp Time.now.to_f end def event_queue @event_queue end def callable? @block end def optimize_changes(changes) @queue_optimizer.smoosh_changes(changes) end def min_delay_between_events @min_delay_between_events end def stopped? listener.state == :stopped end def paused? listener.state == :paused end private attr_reader :listener end end end listen-3.0.3/lib/listen/event/loop.rb000066400000000000000000000045761255413553300175200ustar00rootroot00000000000000require 'thread' require 'timeout' require 'listen/event/processor' module Listen module Event class Loop class Error < RuntimeError class NotStarted < Error end end def initialize(config) @config = config @wait_thread = nil @state = :paused @reasons = ::Queue.new end def wakeup_on_event return if stopped? return unless processing? return unless wait_thread.alive? _wakeup(:event) end def paused? wait_thread && state == :paused end def processing? return false if stopped? return false if paused? state == :processing end def setup # TODO: use a Fiber instead? q = ::Queue.new @wait_thread = Internals::ThreadPool.add do _wait_for_changes(q, config) end Listen::Logger.debug('Waiting for processing to start...') Timeout.timeout(5) { q.pop } end def resume fail Error::NotStarted if stopped? return unless wait_thread _wakeup(:resume) end def pause # TODO: works? # fail NotImplementedError end def teardown return unless wait_thread if wait_thread.alive? _wakeup(:teardown) wait_thread.join end @wait_thread = nil end def stopped? !wait_thread end private attr_reader :config attr_reader :wait_thread attr_accessor :state def _wait_for_changes(ready_queue, config) processor = Event::Processor.new(config, @reasons) _wait_until_resumed(ready_queue) processor.loop_for(config.min_delay_between_events) rescue StandardError => ex _nice_error(ex) end def _sleep(*args) Kernel.sleep(*args) end def _wait_until_resumed(ready_queue) self.state = :paused ready_queue << :ready sleep self.state = :processing end def _nice_error(ex) indent = "\n -- " msg = format( 'exception while processing events: %s Backtrace:%s%s', ex, indent, ex.backtrace * indent ) Listen::Logger.error(msg) end def _wakeup(reason) @reasons << reason wait_thread.wakeup end end end end listen-3.0.3/lib/listen/event/processor.rb000066400000000000000000000057501255413553300205610ustar00rootroot00000000000000module Listen module Event class Processor def initialize(config, reasons) @config = config @reasons = reasons _reset_no_unprocessed_events end # TODO: implement this properly instead of checking the state at arbitrary # points in time def loop_for(latency) @latency = latency loop do _wait_until_events _wait_until_events_calm_down _wait_until_no_longer_paused _process_changes end rescue Stopped Listen::Logger.debug('Processing stopped') end private class Stopped < RuntimeError end def _wait_until_events_calm_down loop do now = _timestamp # Assure there's at least latency between callbacks to allow # for accumulating changes diff = _deadline - now break if diff <= 0 # give events a bit of time to accumulate so they can be # compressed/optimized _sleep(:waiting_until_latency, diff) end end def _wait_until_no_longer_paused # TODO: may not be a good idea? _sleep(:waiting_for_unpause) while config.paused? end def _check_stopped return unless config.stopped? _flush_wakeup_reasons raise Stopped end def _sleep(_local_reason, *args) _check_stopped sleep_duration = config.sleep(*args) _check_stopped _flush_wakeup_reasons do |reason| next unless reason == :event _remember_time_of_first_unprocessed_event unless config.paused? end sleep_duration end def _remember_time_of_first_unprocessed_event @first_unprocessed_event_time ||= _timestamp end def _reset_no_unprocessed_events @first_unprocessed_event_time = nil end def _deadline @first_unprocessed_event_time + @latency end def _wait_until_events # TODO: long sleep may not be a good idea? _sleep(:waiting_for_events) while config.event_queue.empty? @first_unprocessed_event_time ||= _timestamp end def _flush_wakeup_reasons reasons = @reasons until reasons.empty? reason = reasons.pop yield reason if block_given? end end def _timestamp config.timestamp end # for easier testing without sleep loop def _process_changes _reset_no_unprocessed_events changes = [] changes << config.event_queue.pop until config.event_queue.empty? callable = config.callable? return unless callable hash = config.optimize_changes(changes) result = [hash[:modified], hash[:added], hash[:removed]] return if result.all?(&:empty?) block_start = _timestamp config.call(*result) Listen::Logger.debug "Callback took #{_timestamp - block_start} sec" end attr_reader :config end end end listen-3.0.3/lib/listen/event/queue.rb000066400000000000000000000022721255413553300176620ustar00rootroot00000000000000require 'thread' module Listen module Event class Queue class Config def initialize(relative) @relative = relative end def relative? @relative end end def initialize(config, &block) @event_queue = ::Queue.new @block = block @config = config end def <<(args) type, change, dir, path, options = *args fail "Invalid type: #{type.inspect}" unless [:dir, :file].include? type fail "Invalid change: #{change.inspect}" unless change.is_a?(Symbol) fail "Invalid path: #{path.inspect}" unless path.is_a?(String) dir = _safe_relative_from_cwd(dir) event_queue.public_send(:<<, [type, change, dir, path, options]) block.call(args) if block end def empty? event_queue.empty? end def pop event_queue.pop end private attr_reader :event_queue attr_reader :block attr_reader :config def _safe_relative_from_cwd(dir) return dir unless config.relative? dir.relative_path_from(Pathname.pwd) rescue ArgumentError dir end end end end listen-3.0.3/lib/listen/file.rb000066400000000000000000000051051255413553300163320ustar00rootroot00000000000000require 'digest/md5' module Listen class File def self.change(record, rel_path) path = Pathname.new(record.root) + rel_path lstat = path.lstat data = { mtime: lstat.mtime.to_f, mode: lstat.mode } record_data = record.file_data(rel_path) if record_data.empty? record.update_file(rel_path, data) return :added end if data[:mode] != record_data[:mode] record.update_file(rel_path, data) return :modified end if data[:mtime] != record_data[:mtime] record.update_file(rel_path, data) return :modified end return if /1|true/ =~ ENV['LISTEN_GEM_DISABLE_HASHING'] return unless self.inaccurate_mac_time?(lstat) # Check if change happened within 1 second (maybe it's even # too much, e.g. 0.3-0.5 could be sufficient). # # With rb-fsevent, there's a (configurable) latency between # when file was changed and when the event was triggered. # # If a file is saved at ???14.998, by the time the event is # actually received by Listen, the time could already be e.g. # ???15.7. # # And since Darwin adapter uses directory scanning, the file # mtime may be the same (e.g. file was changed at ???14.001, # then at ???14.998, but the fstat time would be ???14.0 in # both cases). # # If change happend at ???14.999997, the mtime is 14.0, so for # an mtime=???14.0 we assume it could even be almost ???15.0 # # So if Time.now.to_f is ???15.999998 and stat reports mtime # at ???14.0, then event was due to that file'd change when: # # ???15.999997 - ???14.999998 < 1.0s # # So the "2" is "1 + 1" (1s to cover rb-fsevent latency + # 1s maximum difference between real mtime and that recorded # in the file system) # return if data[:mtime].to_i + 2 <= Time.now.to_f md5 = Digest::MD5.file(path).digest record.update_file(rel_path, data.merge(md5: md5)) :modified if record_data[:md5] && md5 != record_data[:md5] rescue SystemCallError record.unset_path(rel_path) :removed rescue Listen::Logger.debug "lstat failed for: #{rel_path} (#{$ERROR_INFO})" raise end def self.inaccurate_mac_time?(stat) # 'mac' means Modified/Accessed/Created # Since precision depends on mounted FS (e.g. you can have a FAT partiion # mounted on Linux), check for fields with a remainder to detect this [stat.mtime, stat.ctime, stat.atime].map(&:usec).all?(&:zero?) end end end listen-3.0.3/lib/listen/fsm.rb000066400000000000000000000065051255413553300162050ustar00rootroot00000000000000# Code copied from https://github.com/celluloid/celluloid-fsm module Listen module FSM DEFAULT_STATE = :default # Default state name unless one is explicitly set # Included hook to extend class methods def self.included(klass) klass.send :extend, ClassMethods end module ClassMethods # Obtain or set the default state # Passing a state name sets the default state def default_state(new_default = nil) if new_default @default_state = new_default.to_sym else defined?(@default_state) ? @default_state : DEFAULT_STATE end end # Obtain the valid states for this FSM def states @states ||= {} end # Declare an FSM state and optionally provide a callback block to fire # Options: # * to: a state or array of states this state can transition to def state(*args, &block) if args.last.is_a? Hash # Stringify keys :/ options = args.pop.each_with_object({}) { |(k, v), h| h[k.to_s] = v } else options = {} end args.each do |name| name = name.to_sym default_state name if options['default'] states[name] = State.new(name, options['to'], &block) end end end # Be kind and call super if you must redefine initialize def initialize @state = self.class.default_state end # Obtain the current state of the FSM attr_reader :state def transition(state_name) new_state = validate_and_sanitize_new_state(state_name) return unless new_state transition_with_callbacks!(new_state) end # Immediate state transition with no checks, or callbacks. "Dangerous!" def transition!(state_name) @state = state_name end protected def validate_and_sanitize_new_state(state_name) state_name = state_name.to_sym return if current_state_name == state_name if current_state && !current_state.valid_transition?(state_name) valid = current_state.transitions.map(&:to_s).join(', ') msg = "#{self.class} can't change state from '#{@state}'"\ " to '#{state_name}', only to: #{valid}" fail ArgumentError, msg end new_state = states[state_name] unless new_state return if state_name == default_state fail ArgumentError, "invalid state for #{self.class}: #{state_name}" end new_state end def transition_with_callbacks!(state_name) transition! state_name.name state_name.call(self) end def states self.class.states end def default_state self.class.default_state end def current_state states[@state] end def current_state_name current_state && current_state.name || '' end class State attr_reader :name, :transitions def initialize(name, transitions = nil, &block) @name, @block = name, block @transitions = nil @transitions = Array(transitions).map(&:to_sym) if transitions end def call(obj) obj.instance_eval(&@block) if @block end def valid_transition?(new_state) # All transitions are allowed unless expressly return true unless @transitions @transitions.include? new_state.to_sym end end end end listen-3.0.3/lib/listen/internals/000077500000000000000000000000001255413553300170645ustar00rootroot00000000000000listen-3.0.3/lib/listen/internals/thread_pool.rb000066400000000000000000000007731255413553300217200ustar00rootroot00000000000000module Listen # @private api module Internals module ThreadPool def self.add(&block) Thread.new { block.call }.tap do |th| (@threads ||= Queue.new) << th end end def self.stop return unless @threads ||= nil return if @threads.empty? # return to avoid using possibly stubbed Queue killed = Queue.new killed << @threads.pop.kill until @threads.empty? killed.pop.join until killed.empty? end end end end listen-3.0.3/lib/listen/listener.rb000066400000000000000000000062661255413553300172510ustar00rootroot00000000000000require 'English' require 'listen/version' require 'listen/backend' require 'listen/silencer' require 'listen/silencer/controller' require 'listen/queue_optimizer' require 'listen/fsm' require 'listen/event/loop' require 'listen/event/queue' require 'listen/event/config' require 'listen/listener/config' module Listen class Listener include Listen::FSM # Initializes the directories listener. # # @param [String] directory the directories to listen to # @param [Hash] options the listen options (see Listen::Listener::Options) # # @yield [modified, added, removed] the changed files # @yieldparam [Array] modified the list of modified files # @yieldparam [Array] added the list of added files # @yieldparam [Array] removed the list of removed files # def initialize(*dirs, &block) options = dirs.last.is_a?(Hash) ? dirs.pop : {} @config = Config.new(options) eq_config = Event::Queue::Config.new(@config.relative?) queue = Event::Queue.new(eq_config) { @processor.wakeup_on_event } silencer = Silencer.new rules = @config.silencer_rules @silencer_controller = Silencer::Controller.new(silencer, rules) @backend = Backend.new(dirs, queue, silencer, @config) optimizer_config = QueueOptimizer::Config.new(@backend, silencer) pconfig = Event::Config.new( self, queue, QueueOptimizer.new(optimizer_config), @backend.min_delay_between_events, &block) @processor = Event::Loop.new(pconfig) super() # FSM end default_state :initializing state :initializing, to: :backend_started state :backend_started, to: [:frontend_ready] do backend.start end state :frontend_ready, to: [:processing_events] do processor.setup end state :processing_events, to: [:paused, :stopped] do processor.resume end state :paused, to: [:processing_events, :stopped] do processor.pause end state :stopped, to: [:backend_started] do backend.stop # should be before processor.teardown to halt events ASAP processor.teardown end # Starts processing events and starts adapters # or resumes invoking callbacks if paused def start transition :backend_started if state == :initializing transition :frontend_ready if state == :backend_started transition :processing_events if state == :frontend_ready transition :processing_events if state == :paused end # Stops both listening for events and processing them def stop transition :stopped end # Stops invoking callbacks (messages pile up) def pause transition :paused end # processing means callbacks are called def processing? state == :processing_events end def paused? state == :paused end def ignore(regexps) @silencer_controller.append_ignores(regexps) end def ignore!(regexps) @silencer_controller.replace_with_bang_ignores(regexps) end def only(regexps) @silencer_controller.replace_with_only(regexps) end private attr_reader :processor attr_reader :backend end end listen-3.0.3/lib/listen/listener/000077500000000000000000000000001255413553300167125ustar00rootroot00000000000000listen-3.0.3/lib/listen/listener/config.rb000066400000000000000000000022361255413553300205070ustar00rootroot00000000000000module Listen class Listener class Config DEFAULTS = { # Listener options debug: false, # TODO: is this broken? wait_for_delay: nil, # NOTE: should be provided by adapter if possible relative: false, # Backend selecting options force_polling: false, polling_fallback_message: nil } def initialize(opts) @options = DEFAULTS.merge(opts) @relative = @options[:relative] @min_delay_between_events = @options[:wait_for_delay] @silencer_rules = @options # silencer will extract what it needs end def relative? @relative end def min_delay_between_events @min_delay_between_events end def silencer_rules @silencer_rules end def adapter_instance_options(klass) valid_keys = klass.const_get('DEFAULTS').keys Hash[@options.select { |key, _| valid_keys.include?(key) }] end def adapter_select_options valid_keys = %w(force_polling polling_fallback_message).map(&:to_sym) Hash[@options.select { |key, _| valid_keys.include?(key) }] end end end end listen-3.0.3/lib/listen/logger.rb000066400000000000000000000012701255413553300166710ustar00rootroot00000000000000module Listen def self.logger @logger ||= nil end def self.logger=(logger) @logger = logger end def self.setup_default_logger_if_unset self.logger ||= ::Logger.new(STDERR).tap do |logger| debugging = ENV['LISTEN_GEM_DEBUGGING'] logger.level = case debugging.to_s when /2/ ::Logger::DEBUG when /true|yes|1/i ::Logger::INFO else ::Logger::ERROR end end end class Logger [:fatal, :error, :warn, :info, :debug].each do |meth| define_singleton_method(meth) do |*args, &block| Listen.logger.public_send(meth, *args, &block) if Listen.logger end end end end listen-3.0.3/lib/listen/options.rb000066400000000000000000000011031255413553300171000ustar00rootroot00000000000000module Listen class Options def initialize(opts, defaults) @options = {} given_options = opts.dup defaults.keys.each do |key| @options[key] = given_options.delete(key) || defaults[key] end return if given_options.empty? msg = "Unknown options: #{given_options.inspect}" Listen::Logger.warn msg fail msg end def method_missing(name, *_) return @options[name] if @options.key?(name) msg = "Bad option: #{name.inspect} (valid:#{@options.keys.inspect})" fail NameError, msg end end end listen-3.0.3/lib/listen/queue_optimizer.rb000066400000000000000000000074031255413553300206440ustar00rootroot00000000000000module Listen class QueueOptimizer class Config def initialize(adapter_class, silencer) @adapter_class = adapter_class @silencer = silencer end def exist?(path) Pathname(path).exist? end def silenced?(path, type) @silencer.silenced?(path, type) end def debug(*args, &block) Listen.logger.debug(*args, &block) end end def smoosh_changes(changes) # TODO: adapter could be nil at this point (shutdown) cookies = changes.group_by do |_, _, _, _, options| (options || {})[:cookie] end _squash_changes(_reinterpret_related_changes(cookies)) end def initialize(config) @config = config end private attr_reader :config # groups changes into the expected structure expected by # clients def _squash_changes(changes) # We combine here for backward compatibility # Newer clients should receive dir and path separately changes = changes.map { |change, dir, path| [change, dir + path] } actions = changes.group_by(&:last).map do |path, action_list| [_logical_action_for(path, action_list.map(&:first)), path.to_s] end config.debug("listen: raw changes: #{actions.inspect}") { modified: [], added: [], removed: [] }.tap do |squashed| actions.each do |type, path| squashed[type] << path unless type.nil? end config.debug("listen: final changes: #{squashed.inspect}") end end def _logical_action_for(path, actions) actions << :added if actions.delete(:moved_to) actions << :removed if actions.delete(:moved_from) modified = actions.detect { |x| x == :modified } _calculate_add_remove_difference(actions, path, modified) end def _calculate_add_remove_difference(actions, path, default_if_exists) added = actions.count { |x| x == :added } removed = actions.count { |x| x == :removed } diff = added - removed # TODO: avoid checking if path exists and instead assume the events are # in order (if last is :removed, it doesn't exist, etc.) if config.exist?(path) if diff > 0 :added elsif diff.zero? && added > 0 :modified else default_if_exists end else diff < 0 ? :removed : nil end end # remove extraneous rb-inotify events, keeping them only if it's a possible # editor rename() call (e.g. Kate and Sublime) def _reinterpret_related_changes(cookies) table = { moved_to: :added, moved_from: :removed } cookies.map do |_, changes| data = _detect_possible_editor_save(changes) if data to_dir, to_file = data [[:modified, to_dir, to_file]] else not_silenced = changes.reject do |type, _, _, path, _| config.silenced?(Pathname(path), type) end not_silenced.map do |_, change, dir, path, _| [table.fetch(change, change), dir, path] end end end.flatten(1) end def _detect_possible_editor_save(changes) return unless changes.size == 2 from_type = from_change = from = nil to_type = to_change = to_dir = to = nil changes.each do |data| case data[1] when :moved_from from_type, from_change, _, from, _ = data when :moved_to to_type, to_change, to_dir, to, _ = data else return nil end end return unless from && to # Expect an ignored moved_from and non-ignored moved_to # to qualify as an "editor modify" return unless config.silenced?(Pathname(from), from_type) config.silenced?(Pathname(to), to_type) ? nil : [to_dir, to] end end end listen-3.0.3/lib/listen/record.rb000066400000000000000000000062621255413553300166760ustar00rootroot00000000000000require 'thread' require 'listen/record/entry' require 'listen/record/symlink_detector' module Listen class Record # TODO: one Record object per watched directory? # TODO: deprecate attr_reader :root def initialize(directory) @tree = _auto_hash @root = directory.to_s end def add_dir(rel_path) return if [nil, '', '.'].include? rel_path @tree[rel_path] ||= {} end def update_file(rel_path, data) dirname, basename = Pathname(rel_path).split.map(&:to_s) _fast_update_file(dirname, basename, data) end def unset_path(rel_path) dirname, basename = Pathname(rel_path).split.map(&:to_s) _fast_unset_path(dirname, basename) end def file_data(rel_path) dirname, basename = Pathname(rel_path).split.map(&:to_s) if [nil, '', '.'].include? dirname tree[basename] ||= {} tree[basename].dup else tree[dirname] ||= {} tree[dirname][basename] ||= {} tree[dirname][basename].dup end end def dir_entries(rel_path) subtree = if [nil, '', '.'].include? rel_path.to_s tree else tree[rel_path.to_s] ||= _auto_hash tree[rel_path.to_s] end result = {} subtree.each do |key, values| # only get data for file entries result[key] = values.key?(:mtime) ? values : {} end result end def build @tree = _auto_hash # TODO: test with a file name given # TODO: test other permissions # TODO: test with mixed encoding symlink_detector = SymlinkDetector.new remaining = ::Queue.new remaining << Entry.new(root, nil, nil) _fast_build_dir(remaining, symlink_detector) until remaining.empty? end private def _auto_hash Hash.new { |h, k| h[k] = Hash.new } end def tree @tree end def _fast_update_file(dirname, basename, data) if [nil, '', '.'].include? dirname tree[basename] = (tree[basename] || {}).merge(data) else tree[dirname] ||= {} tree[dirname][basename] = (tree[dirname][basename] || {}).merge(data) end end def _fast_unset_path(dirname, basename) # 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 tree.key?(basename) tree.delete(basename) else return unless tree.key?(dirname) tree[dirname].delete(basename) end end def _fast_build_dir(remaining, symlink_detector) entry = remaining.pop children = entry.children # NOTE: children() implicitly tests if dir symlink_detector.verify_unwatched!(entry) children.each { |child| remaining << child } add_dir(entry.record_dir_key) rescue Errno::ENOTDIR _fast_try_file(entry) rescue SystemCallError, SymlinkDetector::Error _fast_unset_path(entry.relative, entry.name) end def _fast_try_file(entry) _fast_update_file(entry.relative, entry.name, entry.meta) rescue SystemCallError _fast_unset_path(entry.relative, entry.name) end end end listen-3.0.3/lib/listen/record/000077500000000000000000000000001255413553300163435ustar00rootroot00000000000000listen-3.0.3/lib/listen/record/entry.rb000066400000000000000000000024711255413553300200350ustar00rootroot00000000000000module Listen # @private api class Record # Represents a directory entry (dir or file) class Entry # file: "/home/me/watched_dir", "app/models", "foo.rb" # dir, "/home/me/watched_dir", "." def initialize(root, relative, name = nil) @root, @relative, @name = root, relative, name end attr_reader :root, :relative, :name def children child_relative = _join (Dir.entries(sys_path) - %w(. ..)).map do |name| Entry.new(@root, child_relative, name) end end def meta lstat = ::File.lstat(sys_path) { mtime: lstat.mtime.to_f, mode: lstat.mode } end # record hash is e.g. # if @record["/home/me/watched_dir"]["project/app/models"]["foo.rb"] # if @record["/home/me/watched_dir"]["project/app"]["models"] # record_dir_key is "project/app/models" def record_dir_key ::File.join(*[@relative, @name].compact) end def sys_path # Use full path in case someone uses chdir ::File.join(*[@root, @relative, @name].compact) end def real_path @real_path ||= ::File.realpath(sys_path) end private def _join args = [@relative, @name].compact args.empty? ? nil : ::File.join(*args) end end end end listen-3.0.3/lib/listen/record/symlink_detector.rb000066400000000000000000000014721255413553300222530ustar00rootroot00000000000000require 'set' module Listen # @private api class Record class SymlinkDetector WIKI = 'https://github.com/guard/listen/wiki/Duplicate-directory-errors' SYMLINK_LOOP_ERROR = <<-EOS ** ERROR: directory is already being watched! ** Directory: %s is already begin watched through: %s MORE INFO: #{WIKI} EOS class Error < RuntimeError end def initialize @real_dirs = Set.new end def verify_unwatched!(entry) real_path = entry.real_path @real_dirs.add?(real_path) || _fail(entry.sys_path, real_path) end private def _fail(symlinked, real_path) STDERR.puts format(SYMLINK_LOOP_ERROR, symlinked, real_path) fail Error, 'Failed due to looped symlinks' end end end end listen-3.0.3/lib/listen/silencer.rb000066400000000000000000000042701255413553300172210ustar00rootroot00000000000000module Listen class Silencer # The default list of directories that get ignored. DEFAULT_IGNORED_DIRECTORIES = %r{^(?: \.git | \.svn | \.hg | \.rbx | \.bundle | bundle | vendor/bundle | log | tmp |vendor/ruby )(/|$)}x # The default list of files that get ignored. DEFAULT_IGNORED_EXTENSIONS = /(?: # Kate's tmp\/swp files \..*\d+\.new | \.kate-swp # Gedit tmp files | \.goutputstream-.{6} # Intellij files | ___jb_bak___ | ___jb_old___ # Vim swap files and write test | \.sw[px] | \.swpx | ^4913 # Sed temporary files - but without actual words, like 'sedatives' | (?:^ sed (?: [a-zA-Z0-9]{0}[A-Z]{1}[a-zA-Z0-9]{5} | [a-zA-Z0-9]{1}[A-Z]{1}[a-zA-Z0-9]{4} | [a-zA-Z0-9]{2}[A-Z]{1}[a-zA-Z0-9]{3} | [a-zA-Z0-9]{3}[A-Z]{1}[a-zA-Z0-9]{2} | [a-zA-Z0-9]{4}[A-Z]{1}[a-zA-Z0-9]{1} | [a-zA-Z0-9]{5}[A-Z]{1}[a-zA-Z0-9]{0} ) ) # other files | \.DS_Store | \.tmp | ~ )$/x attr_accessor :only_patterns, :ignore_patterns def initialize configure({}) end def configure(options) @only_patterns = options[:only] ? Array(options[:only]) : nil @ignore_patterns = _init_ignores(options[:ignore], options[:ignore!]) end # Note: relative_path is temporarily expected to be a relative Pathname to # make refactoring easier (ideally, it would take a string) # TODO: switch type and path places - and verify def silenced?(relative_path, type) path = relative_path.to_s if only_patterns && type == :file return true unless only_patterns.any? { |pattern| path =~ pattern } end ignore_patterns.any? { |pattern| path =~ pattern } end private attr_reader :options def _init_ignores(ignores, overrides) patterns = [] unless overrides patterns << DEFAULT_IGNORED_DIRECTORIES patterns << DEFAULT_IGNORED_EXTENSIONS end patterns << ignores patterns << overrides patterns.compact.flatten end end end listen-3.0.3/lib/listen/silencer/000077500000000000000000000000001255413553300166715ustar00rootroot00000000000000listen-3.0.3/lib/listen/silencer/controller.rb000066400000000000000000000022611255413553300214020ustar00rootroot00000000000000module Listen class Silencer class Controller def initialize(silencer, default_options) @silencer = silencer opts = default_options @prev_silencer_options = {} rules = [:only, :ignore, :ignore!].map do |option| [option, opts[option]] if opts.key? option end _reconfigure_silencer(Hash[rules.compact]) end def append_ignores(*regexps) prev_ignores = Array(@prev_silencer_options[:ignore]) _reconfigure_silencer(ignore: [prev_ignores + regexps]) end def replace_with_bang_ignores(regexps) _reconfigure_silencer(ignore!: regexps) end def replace_with_only(regexps) _reconfigure_silencer(only: regexps) end private def _reconfigure_silencer(extra_options) opts = extra_options.dup opts = opts.map do |key, value| [key, Array(value).flatten.compact] end opts = Hash[opts] if opts.key?(:ignore) && opts[:ignore].empty? opts.delete(:ignore) end @prev_silencer_options = opts @silencer.configure(@prev_silencer_options.dup.freeze) end end end end listen-3.0.3/lib/listen/version.rb000066400000000000000000000000461255413553300170770ustar00rootroot00000000000000module Listen VERSION = '3.0.3' end listen-3.0.3/listen.gemspec000066400000000000000000000017271255413553300156730ustar00rootroot00000000000000# coding: utf-8 lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'listen/version' Gem::Specification.new do |s| s.name = 'listen' s.version = Listen::VERSION s.license = 'MIT' s.author = 'Thibaud Guillaume-Gentil' s.email = 'thibaud@thibaud.gg' s.homepage = 'https://github.com/guard/listen' s.summary = 'Listen to file modifications' s.description = 'The Listen gem listens to file modifications and '\ 'notifies you about the changes. Works everywhere!' s.files = `git ls-files -z`.split("\x0").select do |f| /^(?:bin|lib)\// =~ f end + %w(CHANGELOG.md CONTRIBUTING.md LICENSE.txt README.md) s.test_files = [] s.executable = 'listen' s.require_path = 'lib' s.required_ruby_version = '>= 1.9.3' s.add_dependency 'rb-fsevent', '>= 0.9.3' s.add_dependency 'rb-inotify', '>= 0.9' s.add_development_dependency 'bundler', '>= 1.3.5' end listen-3.0.3/spec/000077500000000000000000000000001255413553300137535ustar00rootroot00000000000000listen-3.0.3/spec/acceptance/000077500000000000000000000000001255413553300160415ustar00rootroot00000000000000listen-3.0.3/spec/acceptance/listen_spec.rb000066400000000000000000000201331255413553300206750ustar00rootroot00000000000000# encoding: UTF-8 RSpec.describe 'Listen', acceptance: true do let(:base_options) { { latency: 0.1 } } let(:polling_options) { {} } let(:options) { {} } let(:all_options) { base_options.merge(polling_options).merge(options) } let(:wrapper) { setup_listener(all_options, :track_changes) } before { wrapper.listener.start } after { wrapper.listener.stop } subject { wrapper } context 'with one listen dir' do let(:paths) { Pathname.new(Dir.pwd) } around { |example| fixtures { example.run } } context 'with change block raising' do let(:callback) { ->(_, _, _) { fail 'foo' } } let(:wrapper) { setup_listener(all_options, callback) } it 'warns the backtrace' do expect(Listen::Logger).to receive(:error). with(/exception while processing events: foo.*Backtrace:/) wrapper.listen { touch 'file.rb' } end end modes = case ENV['TEST_LISTEN_ADAPTER_MODES'] || 'both' when 'polling' [true] when 'native' [false] else [false, true] end # TODO: make it configurable # TODO: restore modes.each do |polling| context "force_polling option to #{polling}" do let(:polling_options) { { force_polling: polling } } if polling context 'when polling' do context 'with a large latency' do let(:options) { { latency: 10 } } it 'passes the latency option correctly' do expect(subject).to_not process_addition_of('file.rb') end end end else unless darwin? context 'when driver does not support option' do let(:options) { { latency: 10 } } it 'does not pass the latency option' do expect(subject).to process_addition_of('file.rb') end end 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') } 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 '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 end context 'hidden file in listen dir' do around do |example| change_fs(:added, '.hidden') example.run end it { is_expected.to process_modification_of('.hidden') } end context 'dir in listen dir' do around do |example| mkdir_p 'dir' example.run end it { is_expected.to process_addition_of('dir/file.rb') } end 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 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 end context 'with .bundle dir ignored by default' do around do |example| mkdir_p '.bundle' example.run 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 end end end end listen-3.0.3/spec/lib/000077500000000000000000000000001255413553300145215ustar00rootroot00000000000000listen-3.0.3/spec/lib/listen/000077500000000000000000000000001255413553300160175ustar00rootroot00000000000000listen-3.0.3/spec/lib/listen/adapter/000077500000000000000000000000001255413553300174375ustar00rootroot00000000000000listen-3.0.3/spec/lib/listen/adapter/base_spec.rb000066400000000000000000000047051255413553300217160ustar00rootroot00000000000000RSpec.describe Listen::Adapter::Base do class FakeAdapter < described_class def initialize(config) @my_callbacks = {} super end def _run fail NotImplementedError end def _configure(dir, &callback) @my_callbacks[dir.to_s] = callback end def fake_event(event) dir = event[:dir] @my_callbacks[dir].call(event) end def _process_event(dir, event) _queue_change(:file, dir, event[:file], cookie: event[:cookie]) end end let(:dir1) { instance_double(Pathname, 'dir1', to_s: '/foo/dir1') } let(:config) { instance_double(Listen::Adapter::Config) } let(:queue) { instance_double(Queue) } let(:silencer) { instance_double(Listen::Silencer) } let(:adapter_options) { {} } let(:snapshot) { instance_double(Listen::Change) } let(:record) { instance_double(Listen::Record) } subject { FakeAdapter.new(config) } before do allow(config).to receive(:directories).and_return([dir1]) allow(config).to receive(:queue).and_return(queue) allow(config).to receive(:silencer).and_return(silencer) allow(config).to receive(:adapter_options).and_return(adapter_options) allow(Listen::Internals::ThreadPool). to receive(:add) { |&block| block.call } # Stuff that happens in configure() 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) end describe '#start' do before do allow(subject).to receive(:_run) allow(snapshot).to receive(:record).and_return(record) allow(record).to receive(:build) end it 'builds record' do expect(record).to receive(:build) subject.start end it 'runs the adapter' do expect(subject).to receive(:_run) subject.start end end describe 'handling events' do before do allow(subject).to receive(:_run) allow(snapshot).to receive(:record).and_return(record) allow(record).to receive(:build) end context 'when an event occurs' do it 'passes invalidates the snapshot based on the event' do subject.start expect(snapshot).to receive(:invalidate).with(:file, 'bar', cookie: 3) event = { dir: '/foo/dir1', file: 'bar', type: :moved, cookie: 3 } subject.fake_event(event) end end end end listen-3.0.3/spec/lib/listen/adapter/bsd_spec.rb000066400000000000000000000003051255413553300215440ustar00rootroot00000000000000RSpec.describe Listen::Adapter::BSD do describe 'class' do subject { described_class } if bsd? it { should be_usable } else it { should_not be_usable } end end end listen-3.0.3/spec/lib/listen/adapter/config_spec.rb000066400000000000000000000057251255413553300222540ustar00rootroot00000000000000require 'listen/adapter/config' RSpec.describe Listen::Adapter::Config do let(:directories) { [path1, path2] } let(:queue) { instance_double(Queue) } let(:silencer) { instance_double(Listen::Silencer) } # NOTE: defaults are handled later in Listen::Options let(:adapter_options) { { latency: 1.234 } } subject do described_class.new(directories, queue, silencer, adapter_options) end # Here's what may be passed to initializer let(:path1) { fake_path('/real/path1', realpath: real_path1) } let(:path2) { fake_path('/real/path2', realpath: real_path2) } let(:symlinked_dir1) { fake_path('symlinked_dir1', realpath: real_path1) } let(:symlinked_dir2) { fake_path('symlinked_dir1', realpath: real_path2) } # Here's what expected to be returned (just so that realpath() calls return # something useful) let(:real_path1) { fake_path('/real/path1') } let(:real_path2) { fake_path('/real/path2') } before do allow(Pathname).to receive(:new) do |*args| fail "unstubbed Pathname.new(#{args.map(&:inspect) * ','})" end allow(Pathname).to receive(:new).with('/real/path1').and_return(path1) allow(Pathname).to receive(:new).with('/real/path2').and_return(path2) allow(Pathname).to receive(:new).with(path1).and_return(path1) allow(Pathname).to receive(:new).with(path2).and_return(path2) allow(Pathname).to receive(:new).with('symlinked_dir1'). and_return(symlinked_dir1) allow(Pathname).to receive(:new).with('symlinked_dir2'). and_return(symlinked_dir2) end describe '#initialize' do context 'with directories as array' do context 'with strings for directories' do context 'when already resolved' do let(:directories) { ['/real/path1', '/real/path2'] } it 'returns array of pathnames' do expect(subject.directories).to eq([real_path1, real_path2]) end end context 'when not resolved' do let(:directories) { ['symlinked_dir1', 'symlinked_dir2'] } it 'returns array of resolved pathnames' do expect(subject.directories).to eq([real_path1, real_path2]) end end end context 'with Pathnames for directories' do let(:directories) { [path1, path2] } it 'returns array of pathnames' do expect(subject.directories).to eq([real_path1, real_path2]) end end end context 'with directories as messy array' do pending 'implement me' end context 'with no directories' do pending 'implement me' end end describe '#adapter_options' do it 'provides a set of adapter_specific options' do expect(subject.adapter_options).to eq(latency: 1.234) end end describe '#queue' do it 'provides a direct queue for filesystem events' do expect(subject.queue).to eq(queue) end end describe '#silencer' do it 'provides a silencer object' do expect(subject.silencer).to eq(silencer) end end end listen-3.0.3/spec/lib/listen/adapter/darwin_spec.rb000066400000000000000000000115541255413553300222700ustar00rootroot00000000000000# This is just so stubs work require 'rb-fsevent' require 'listen/adapter/darwin' include Listen RSpec.describe Adapter::Darwin do describe 'class' do subject { described_class } if darwin? it { should be_usable } else it { should_not be_usable } end end let(:options) { {} } let(:config) { instance_double(Listen::Adapter::Config) } let(:queue) { instance_double(::Queue) } let(:silencer) { instance_double(Listen::Silencer) } let(:dir1) { fake_path('/foo/dir1', cleanpath: fake_path('/foo/dir1')) } let(:directories) { [dir1] } subject { described_class.new(config) } before do allow(config).to receive(:directories).and_return(directories) allow(config).to receive(:adapter_options).and_return(options) end describe '#_latency' do subject { described_class.new(config).options.latency } context 'with no overriding option' do it { should eq 0.1 } end context 'with custom latency overriding' do let(:options) { { latency: 1234 } } it { should eq 1234 } end end describe 'multiple dirs' do let(:dir1) { fake_path('/foo/dir1', cleanpath: fake_path('/foo/dir1')) } let(:dir2) { fake_path('/foo/dir2', cleanpath: fake_path('/foo/dir1')) } let(:dir3) { fake_path('/foo/dir3', cleanpath: fake_path('/foo/dir1')) } before do allow(config).to receive(:queue).and_return(queue) allow(config).to receive(:silencer).and_return(silencer) end let(:foo1) { double('fsevent1') } let(:foo2) { double('fsevent2') } let(:foo3) { double('fsevent3') } before do allow(FSEvent).to receive(:new).and_return(*expectations.values, nil) expectations.each do |dir, obj| allow(obj).to receive(:watch).with(dir.to_s, latency: 0.1) end subject.configure end describe 'configuration' do context 'with 1 directory' do let(:directories) { expectations.keys.map { |p| Pathname(p.to_s) } } let(:expectations) { { '/foo/dir1': foo1 } } it 'configures directory' do expect(foo1).to have_received(:watch).with('/foo/dir1', latency: 0.1) end end context 'with 2 directories' do let(:directories) { expectations.keys.map { |p| Pathname(p.to_s) } } let(:expectations) { { dir1: foo1, dir2: foo2 } } it 'configures directories' do expect(foo1).to have_received(:watch).with('dir1', latency: 0.1) expect(foo2).to have_received(:watch).with('dir2', latency: 0.1) end end context 'with 3 directories' do let(:directories) { expectations.keys.map { |p| Pathname(p.to_s) } } let(:expectations) do { '/foo/dir1': foo1, '/foo/dir2': foo2, '/foo/dir3': foo3 } end it 'configures directories' do expect(foo1).to have_received(:watch).with('/foo/dir1', latency: 0.1) expect(foo2).to have_received(:watch).with('/foo/dir2', latency: 0.1) expect(foo3).to have_received(:watch).with('/foo/dir3', latency: 0.1) end end end describe 'running threads' do let(:running) { [] } let(:directories) { expectations.keys.map { |p| Pathname(p.to_s) } } before do started = ::Queue.new threads = ::Queue.new left = ::Queue.new # NOTE: Travis has a hard time creating threads on OSX thread_start_overhead = 3 max_test_time = 3 * thread_start_overhead block_time = max_test_time + thread_start_overhead expectations.each do |name, _| left << name end expectations.each do |_, obj| allow(obj).to receive(:run) do current_name = left.pop threads << Thread.current started << current_name sleep block_time end end Timeout.timeout(max_test_time) do subject.start until started.size == expectations.size sleep 0.1 end end running << started.pop until started.empty? killed = ::Queue.new killed << threads.pop.kill until threads.empty? killed.pop.join until killed.empty? end context 'with 1 directory' do let(:expectations) { { dir1: foo1 } } it 'runs all the workers without blocking' do expect(running.sort).to eq(expectations.keys) end end context 'with 2 directories' do let(:expectations) { { dir1: foo1, dir2: foo2 } } it 'runs all the workers without blocking' do expect(running.sort).to eq(expectations.keys) end end context 'with 3 directories' do let(:expectations) { { dir1: foo1, dir2: foo2, dir3: foo3 } } it 'runs all the workers without blocking' do expect(running.sort).to eq(expectations.keys) end end end end end listen-3.0.3/spec/lib/listen/adapter/linux_spec.rb000066400000000000000000000077221255413553300221450ustar00rootroot00000000000000RSpec.describe Listen::Adapter::Linux do describe 'class' do subject { described_class } if linux? it { should be_usable } else it { should_not be_usable } end end if linux? let(:dir1) do instance_double( Pathname, 'dir1', to_s: '/foo/dir1', cleanpath: real_dir1 ) end # just so cleanpath works in above double let(:real_dir1) { instance_double(Pathname, 'dir1', to_s: '/foo/dir1') } let(:config) { instance_double(Listen::Adapter::Config) } let(:queue) { instance_double(Queue) } let(:silencer) { instance_double(Listen::Silencer) } let(:snapshot) { instance_double(Listen::Change) } let(:record) { instance_double(Listen::Record) } # TODO: fix other adapters too! subject { described_class.new(config) } describe 'inotify limit message' do let(:directories) { [Pathname.pwd] } let(:adapter_options) { {} } before do require 'rb-inotify' fake_worker = double(:fake_worker) allow(fake_worker).to receive(:watch).and_raise(Errno::ENOSPC) 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 '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] } } before do allow(Kernel).to receive(:require).with('rb-inotify') 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) subject.configure end 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 end end listen-3.0.3/spec/lib/listen/adapter/polling_spec.rb000066400000000000000000000041141255413553300224420ustar00rootroot00000000000000include Listen RSpec.describe Adapter::Polling do describe 'class' do subject { described_class } it { should be_usable } end subject do described_class.new(config) end let(:dir1) do instance_double(Pathname, 'dir1', to_s: '/foo/dir1', cleanpath: real_dir1) end # just so cleanpath works in above double let(:real_dir1) { instance_double(Pathname, 'dir1', to_s: '/foo/dir1') } let(:config) { instance_double(Listen::Adapter::Config) } let(:directories) { [dir1] } let(:options) { {} } let(:queue) { instance_double(Queue) } let(:silencer) { instance_double(Listen::Silencer) } let(:snapshot) { instance_double(Listen::Change) } let(:record) { instance_double(Listen::Record) } context 'with a valid configuration' do before do allow(config).to receive(:directories).and_return(directories) allow(config).to receive(:adapter_options).and_return(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).to receive(:new).with(config, record). and_return(snapshot) allow(Listen::Change::Config).to receive(:new).with(queue, silencer). and_return(config) end describe '#start' do before do allow(snapshot).to receive(:record).and_return(record) allow(record).to receive(:build) end it 'notifies change on every listener directories path' do expect(snapshot).to receive(:invalidate). with(:dir, '.', recursive: true) t = Thread.new { subject.start } sleep 0.25 t.kill t.join end end describe '#_latency' do subject do adapter = described_class.new(config) adapter.options.latency end context 'with no overriding option' do it { should eq 1.0 } end context 'with custom latency overriding' do let(:options) { { latency: 1234 } } it { should eq 1234 } end end end end listen-3.0.3/spec/lib/listen/adapter/windows_spec.rb000066400000000000000000000003151255413553300224670ustar00rootroot00000000000000RSpec.describe Listen::Adapter::Windows do describe 'class' do subject { described_class } if windows? it { should be_usable } else it { should_not be_usable } end end end listen-3.0.3/spec/lib/listen/adapter_spec.rb000066400000000000000000000045271255413553300210060ustar00rootroot00000000000000RSpec.describe Listen::Adapter do let(:listener) { instance_double(Listen::Listener, options: {}) } before do allow(Listen::Adapter::BSD).to receive(:usable?) { false } allow(Listen::Adapter::Darwin).to receive(:usable?) { false } allow(Listen::Adapter::Linux).to receive(:usable?) { false } allow(Listen::Adapter::Windows).to receive(:usable?) { false } end describe '.select' do it 'returns Polling adapter if forced' do klass = Listen::Adapter.select(force_polling: true) expect(klass).to eq Listen::Adapter::Polling end 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 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 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 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 end context 'no usable adapters' do before { allow(Kernel).to receive(:warn) } it 'returns Polling adapter' do klass = Listen::Adapter.select(force_polling: true) expect(klass).to eq Listen::Adapter::Polling end it 'warns polling fallback with default message' do msg = described_class::POLLING_FALLBACK_MESSAGE expect(Kernel).to receive(:warn).with("[Listen warning]:\n #{msg}") Listen::Adapter.select end it "doesn't warn if polling_fallback_message is false" do expect(Kernel).to_not receive(:warn) Listen::Adapter.select(polling_fallback_message: false) end it 'warns polling fallback with custom message if set' do expected_msg = "[Listen warning]:\n custom fallback message" expect(Kernel).to receive(:warn).with(expected_msg) msg = 'custom fallback message' Listen::Adapter.select(polling_fallback_message: msg) end end end end listen-3.0.3/spec/lib/listen/backend_spec.rb000066400000000000000000000043341255413553300207510ustar00rootroot00000000000000require 'listen/backend' RSpec.describe Listen::Backend do let(:dir1) { instance_double(Pathname, 'dir1', to_s: '/foo/dir1') } let(:silencer) { instance_double(Listen::Silencer) } let(:queue) { instance_double(Queue) } let(:select_options) do { force_polling: false, polling_fallback_message: 'foo' } end let(:adapter_options) { { latency: 1234 } } let(:options) { select_options.merge(adapter_options) } let(:adapter_config_class) { class_double('Listen::Adapter::Config') } let(:adapter_config) { instance_double('Listen::Adapter::Config') } let(:config) { instance_double(Listen::Listener::Config) } subject { described_class.new([dir1], queue, silencer, config) } # Use Polling since it has a valid :latency option let(:adapter_defaults) { { latency: 5.4321 } } let(:adapter_class) { Listen::Adapter::Polling } let(:adapter) { instance_double('Listen::Adapter::Polling') } let(:config_min_delay_between_events) { 0.1234 } before do stub_const('Listen::Adapter::Config', adapter_config_class) allow(adapter_config_class).to receive(:new). with([dir1], queue, silencer, adapter_options). and_return(adapter_config) allow(Listen::Adapter).to receive(:select). with(select_options).and_return(adapter_class) allow(adapter_class).to receive(:new). with(adapter_config).and_return(adapter) allow(Listen::Adapter::Polling).to receive(:new).with(adapter_config). and_return(adapter) allow(config).to receive(:adapter_select_options). and_return(select_options) allow(config).to receive(:adapter_instance_options). and_return(adapter_options) allow(config).to receive(:min_delay_between_events). and_return(config_min_delay_between_events) end describe '#initialize' do context 'with config' do it 'sets up an adapter class' do expect(adapter_class).to receive(:new). with(adapter_config).and_return(adapter) subject end end end describe '#start' do it 'starts the adapter' do expect(adapter).to receive(:start) subject.start end end describe '#stop' do it 'stops the adapter' do # TODO: does nothing for now subject.stop end end end listen-3.0.3/spec/lib/listen/change_spec.rb000066400000000000000000000062701255413553300206100ustar00rootroot00000000000000RSpec.describe Listen::Change do let(:config) { instance_double(Listen::Change::Config) } let(:dir) { instance_double(Pathname) } let(:record) { instance_double(Listen::Record, root: '/dir') } subject { Listen::Change.new(config, record) } let(:full_file_path) { instance_double(Pathname, to_s: '/dir/file.rb') } let(:full_dir_path) { instance_double(Pathname, to_s: '/dir') } before do allow(dir).to receive(:+).with('file.rb') { full_file_path } allow(dir).to receive(:+).with('dir1') { full_dir_path } end describe '#change' do before do allow(config).to receive(:silenced?).and_return(false) end context 'with build options' do it 'calls still_building! on record' do allow(config).to receive(:queue) allow(Listen::File).to receive(:change) subject.invalidate(:file, 'file.rb', build: true) end end context 'file' do context 'with known change' do it 'notifies change directly to listener' do expect(config).to receive(:queue). with(:file, :modified, Pathname.new('/dir'), 'file.rb', {}) subject.invalidate(:file, 'file.rb', change: :modified) end it "doesn't notify to listener if path is silenced" do expect(config).to receive(:silenced?).and_return(true) expect(config).to_not receive(:queue) subject.invalidate(:file, 'file.rb', change: :modified) end end context 'with unknown change' do it 'calls Listen::File#change' do expect(Listen::File).to receive(:change).with(record, 'file.rb') subject.invalidate(:file, 'file.rb', {}) end it "doesn't call Listen::File#change if path is silenced" do expect(config).to receive(:silenced?). with('file.rb', :file).and_return(true) expect(Listen::File).to_not receive(:change) subject.invalidate(:file, 'file.rb', {}) end context 'that returns a change' do before { allow(Listen::File).to receive(:change) { :modified } } context 'listener listen' do it 'notifies change to listener' do expect(config).to receive(:queue). with(:file, :modified, Pathname.new('/dir'), 'file.rb') subject.invalidate(:file, 'file.rb', {}) end context 'silence option' do it 'notifies change to listener' do expect(config).to_not receive(:queue) subject.invalidate(:file, 'file.rb', silence: true) end end end end context 'that returns no change' do before { allow(Listen::File).to receive(:change) { nil } } it "doesn't notifies no change" do expect(config).to_not receive(:queue) subject.invalidate(:file, 'file.rb', {}) end end end end context 'directory' do let(:dir_options) { { recursive: true } } it 'calls Listen::Directory#new' do expect(Listen::Directory).to receive(:scan). with(subject, 'dir1', dir_options) subject.invalidate(:dir, 'dir1', dir_options) end end end end listen-3.0.3/spec/lib/listen/cli_spec.rb000066400000000000000000000057441255413553300201370ustar00rootroot00000000000000require 'listen/cli' RSpec.describe Listen::CLI do let(:options) { {} } let(:forwarder) { instance_double(Listen::Forwarder) } before do allow(forwarder).to receive(:start) end describe 'directories option' do context 'not specified' do let(:options) { %w[] } it 'is set to local directory' do expect(Listen::Forwarder).to receive(:new) do |options| expect(options[:directory]).to eq('.') forwarder end described_class.start(options) end end context 'with a single directory' do let(:options) { %w[-d app] } it 'is set to app' do expect(Listen::Forwarder).to receive(:new) do |options| expect(options[:directory]).to eq(['app']) forwarder end described_class.start(options) end end context 'with a multiple directories' do let(:options) { %w[-d app spec] } it 'is set to an array of the directories' do expect(Listen::Forwarder).to receive(:new) do |options| expect(options[:directory]).to eq(%w(app spec)) forwarder end described_class.start(options) end end end describe 'relative option' do context 'without relative option' do let(:options) { %w[] } it 'is set to false' do expect(Listen::Forwarder).to receive(:new) do |options| expect(options[:relative]).to be(false) forwarder end described_class.start(options) end end context 'when -r' do let(:options) { %w[-r] } it 'is set to true' do expect(Listen::Forwarder).to receive(:new) do |options| expect(options[:relative]).to be(true) forwarder end described_class.start(options) end end context 'when --relative' do let(:options) { %w[--relative] } it 'supports -r option' do expect(Listen::Forwarder).to receive(:new) do |options| expect(options[:relative]).to be(true) forwarder end described_class.start(options) end it 'supports --relative option' do expect(Listen::Forwarder).to receive(:new) do |options| expect(options[:relative]).to be(true) forwarder end described_class.start(options) end end end end RSpec.describe Listen::Forwarder do let(:logger) { instance_double(Logger) } let(:listener) { instance_double(Listen::Listener) } before do allow(Logger).to receive(:new).and_return(logger) allow(logger).to receive(:level=) allow(logger).to receive(:formatter=) allow(logger).to receive(:info) allow(listener).to receive(:start) allow(listener).to receive(:processing?).and_return false end it 'passes relative option to Listen' do value = double('value') expect(Listen).to receive(:to). with(nil, hash_including(relative: value)). and_return(listener) described_class.new(relative: value).start end end listen-3.0.3/spec/lib/listen/directory_spec.rb000066400000000000000000000173031255413553300213660ustar00rootroot00000000000000include Listen RSpec.describe Directory do def fake_file_stat(name, options = {}) defaults = { directory?: false } instance_double(::File::Stat, name, defaults.merge(options)) end def fake_dir_stat(name, options = {}) defaults = { directory?: true } instance_double(::File::Stat, name, defaults.merge(options)) end let(:dir) { double(:dir) } let(:file) { fake_path('file.rb') } let(:file2) { fake_path('file2.rb') } let(:subdir) { fake_path('subdir') } let(:record) do instance_double( Record, root: 'some_dir', dir_entries: record_entries, add_dir: true, unset_path: true) end let(:snapshot) { instance_double(Change, record: record, invalidate: nil) } before do allow(dir).to receive(:+).with('.') { dir } allow(dir).to receive(:+).with('file.rb') { file } allow(dir).to receive(:+).with('subdir') { subdir } allow(file).to receive(:relative_path_from).with(dir) { 'file.rb' } allow(file2).to receive(:relative_path_from).with(dir) { 'file2.rb' } allow(subdir).to receive(:relative_path_from).with(dir) { 'subdir' } allow(Pathname).to receive(:new).with('some_dir').and_return(dir) allow(Pathname).to receive(:new).with('.').and_return(dir) allow(::File).to receive(:lstat) do |*args| fail "Not stubbed: File.lstat(#{args.map(&:inspect) * ','})" end end context '#scan with recursive off' do let(:options) { { recursive: false } } context 'with file & subdir in record' do let(:record_entries) do { 'file.rb' => { mtime: 1.1 }, 'subdir' => {} }.freeze end context 'with empty dir' do before { allow(dir).to receive(:children) { [] } } it 'sets record dir path' do expect(record).to receive(:add_dir).with('.') described_class.scan(snapshot, '.', options) end it "snapshots changes for file path and dir that doesn't exist" do expect(snapshot).to receive(:invalidate).with(:file, 'file.rb', {}) expect(snapshot).to receive(:invalidate). with(:dir, 'subdir', recursive: false) described_class.scan(snapshot, '.', options) end end context 'when subdir is removed' do before do allow(dir).to receive(:children) { [file] } allow(::File).to receive(:lstat).with('file.rb'). and_return(fake_file_stat('file.rb')) end it 'notices subdir does not exist' do expect(snapshot).to receive(:invalidate). with(:dir, 'subdir', recursive: false) described_class.scan(snapshot, '.', options) end end context 'when file.rb removed' do before do allow(dir).to receive(:children) { [subdir] } allow(::File).to receive(:lstat).with('subdir'). and_return(fake_dir_stat('subdir')) end it 'notices file was removed' do expect(snapshot).to receive(:invalidate).with(:file, 'file.rb', {}) described_class.scan(snapshot, '.', options) end end context 'when file2.rb is added' do before do allow(dir).to receive(:children) { [file, file2, subdir] } allow(::File).to receive(:lstat).with('file.rb'). and_return(fake_file_stat('file.rb')) allow(::File).to receive(:lstat).with('file2.rb'). and_return(fake_file_stat('file2.rb')) allow(::File).to receive(:lstat).with('subdir'). and_return(fake_dir_stat('subdir')) end it 'notices file removed and file2 changed' do expect(snapshot).to receive(:invalidate).with(:file, 'file2.rb', {}) described_class.scan(snapshot, '.', options) end end end context 'with empty record' do let(:record_entries) { {} } context 'with non-existing dir path' do before { allow(dir).to receive(:children) { fail Errno::ENOENT } } it 'reports no changes' do expect(snapshot).to_not receive(:invalidate) described_class.scan(snapshot, '.', options) end it 'unsets record dir path' do expect(record).to receive(:unset_path).with('.') described_class.scan(snapshot, '.', options) end end context 'when network share is disconnected' do before { allow(dir).to receive(:children) { fail Errno::EHOSTDOWN } } it 'reports no changes' do expect(snapshot).to_not receive(:invalidate) described_class.scan(snapshot, '.', options) end it 'unsets record dir path' do expect(record).to receive(:unset_path).with('.') described_class.scan(snapshot, '.', options) end end context 'with file.rb in dir' do before do allow(dir).to receive(:children) { [file] } allow(::File).to receive(:lstat).with('file.rb'). and_return(fake_file_stat('file.rb')) end it 'snapshots changes for file & file2 paths' do expect(snapshot).to receive(:invalidate). with(:file, 'file.rb', {}) expect(snapshot).to_not receive(:invalidate). with(:file, 'file2.rb', {}) expect(snapshot).to_not receive(:invalidate). with(:dir, 'subdir', recursive: false) described_class.scan(snapshot, '.', options) end end end end context '#scan with recursive on' do let(:options) { { recursive: true } } context 'with file.rb & subdir in record' do let(:record_entries) do { 'file.rb' => { mtime: 1.1 }, 'subdir' => {} } end context 'with empty dir' do before do allow(dir).to receive(:children) { [] } end it 'snapshots changes for file & subdir path' do expect(snapshot).to receive(:invalidate).with(:file, 'file.rb', {}) expect(snapshot).to receive(:invalidate). with(:dir, 'subdir', recursive: true) described_class.scan(snapshot, '.', options) end end context 'with subdir2 path present' do let(:subdir2) { fake_path('subdir2', children: []) } before do allow(dir).to receive(:children) { [subdir2] } allow(subdir2).to receive(:relative_path_from).with(dir) { 'subdir2' } allow(::File).to receive(:lstat).with('subdir2'). and_return(fake_dir_stat('subdir2')) end it 'snapshots changes for file, file2 & subdir paths' do expect(snapshot).to receive(:invalidate).with(:file, 'file.rb', {}) expect(snapshot).to receive(:invalidate). with(:dir, 'subdir', recursive: true) expect(snapshot).to receive(:invalidate). with(:dir, 'subdir2', recursive: true) described_class.scan(snapshot, '.', options) end end end context 'with empty record' do let(:record_entries) { {} } context 'with non-existing dir' do before do allow(dir).to receive(:children) { fail Errno::ENOENT } end it 'reports no changes' do expect(snapshot).to_not receive(:invalidate) described_class.scan(snapshot, '.', options) end end context 'with subdir present in dir' do before do allow(dir).to receive(:children) { [subdir] } allow(subdir).to receive(:children) { [] } allow(::File).to receive(:lstat).with('subdir'). and_return(fake_dir_stat('subdir')) end it 'snapshots changes for subdir' do expect(snapshot).to receive(:invalidate). with(:dir, 'subdir', recursive: true) described_class.scan(snapshot, '.', options) end end end end end listen-3.0.3/spec/lib/listen/event/000077500000000000000000000000001255413553300171405ustar00rootroot00000000000000listen-3.0.3/spec/lib/listen/event/config_spec.rb000066400000000000000000000013751255413553300217520ustar00rootroot00000000000000require 'listen/event/config' RSpec.describe Listen::Event::Config do let(:listener) { instance_double(Listen::Listener) } let(:event_queue) { instance_double(Listen::Event::Queue) } let(:queue_optimizer) { instance_double(Listen::QueueOptimizer) } let(:wait_for_delay) { 1.234 } context 'with a given block' do let(:myblock) { instance_double(Proc) } subject do described_class.new( listener, event_queue, queue_optimizer, wait_for_delay) do |*args| myblock.call(*args) end end it 'calls the block' do expect(myblock).to receive(:call).with(:foo, :bar) subject.call(:foo, :bar) end it 'is callable' do expect(subject).to be_callable end end end listen-3.0.3/spec/lib/listen/event/loop_spec.rb000066400000000000000000000124221255413553300214510ustar00rootroot00000000000000require 'thread' require 'listen/event/config' require 'listen/event/loop' require 'listen/internals/thread_pool' RSpec.describe Listen::Event::Loop do let(:config) { instance_double(Listen::Event::Config, 'config') } let(:processor) { instance_double(Listen::Event::Processor, 'processor') } let(:thread) { instance_double(Thread) } let(:reasons) { instance_double(::Queue, 'reasons') } let(:ready) { instance_double(::Queue, 'ready') } let(:blocks) do { thread_block: proc { fail 'thread block stub called' }, timer_block: proc { fail 'thread block stub called' }, } end subject { described_class.new(config) } # TODO: this is hideous before do allow(::Queue).to receive(:new).and_return(reasons, ready) allow(Listen::Event::Processor).to receive(:new).with(config, reasons). and_return(processor) allow(Listen::Internals::ThreadPool).to receive(:add) do |*args, &block| fail 'Unstubbed call:'\ " ThreadPool.add(#{args.map(&:inspect) * ','},&#{block.inspect})" end allow(config).to receive(:min_delay_between_events).and_return(1.234) allow(Listen::Internals::ThreadPool).to receive(:add) do |*_, &block| blocks[:thread_block] = block thread end allow(Timeout).to receive(:timeout) do |*_args, &block| blocks[:timer_block] = block end allow(Kernel).to receive(:sleep) do |*args| fail "stub called: sleep(#{args.map(&:inspect) * ','})" end allow(subject).to receive(:_nice_error) do |ex| indent = "\n -- " backtrace = ex.backtrace.reject { |line| line =~ /\/gems\// } fail "error called: #{ex}: #{indent}#{backtrace * indent}" end end describe '#setup' do before do allow(thread).to receive(:wakeup) allow(thread).to receive(:alive?).and_return(true) allow(config).to receive(:min_delay_between_events).and_return(1.234) allow(ready).to receive(:<<).with(:ready) end it 'sets up the thread in a resumable state' do subject.setup expect(subject).to receive(:sleep).with(no_args).ordered allow(processor).to receive(:loop_for).with(1.234).ordered blocks[:thread_block].call end end context 'when stopped' do context 'when resume is called' do it 'fails' do expect { subject.resume }. to raise_error(Listen::Event::Loop::Error::NotStarted) end end context 'when wakeup_on_event is called' do it 'does nothing' do subject.wakeup_on_event end end end context 'when resumed' do before do subject.setup allow(thread).to receive(:wakeup) do allow(subject).to receive(:sleep).with(no_args).ordered allow(processor).to receive(:loop_for).with(1.234).ordered allow(ready).to receive(:<<).with(:ready) blocks[:thread_block].call end allow(reasons).to receive(:<<).with(:resume) subject.resume end it 'is not paused' do expect(subject).to_not be_paused end context 'when resume is called again' do it 'does nothing' do subject.resume end end context 'when wakeup_on_event is called' do let(:epoch) { 1234 } context 'when thread is alive' do before do allow(reasons).to receive(:<<) allow(thread).to receive(:alive?).and_return(true) end it 'wakes up the thread' do expect(thread).to receive(:wakeup) subject.wakeup_on_event end it 'sets the reason for waking up' do expect(reasons).to receive(:<<).with(:event) subject.wakeup_on_event end end context 'when thread is dead' do before do allow(thread).to receive(:alive?).and_return(false) end it 'does not wake up the thread' do expect(thread).to_not receive(:wakeup) subject.wakeup_on_event end end end end context 'when set up / paused' do before do allow(thread).to receive(:alive?).and_return(true) allow(config).to receive(:min_delay_between_events).and_return(1.234) allow(thread).to receive(:wakeup) subject.setup allow(subject).to receive(:sleep).with(no_args).ordered do allow(processor).to receive(:loop_for).with(1.234) blocks[:timer_block].call end allow(ready).to receive(:<<).with(:ready) allow(ready).to receive(:pop) blocks[:thread_block].call end describe '#resume' do before do allow(reasons).to receive(:<<) allow(thread).to receive(:wakeup) end it 'resumes the thread' do expect(thread).to receive(:wakeup) subject.resume end it 'sets the reason for waking up' do expect(reasons).to receive(:<<).with(:resume) subject.resume end end describe '#teardown' do before do allow(reasons).to receive(:<<) allow(thread).to receive(:join) end it 'frees the thread' do subject.teardown end it 'waits for the thread to finish' do expect(thread).to receive(:join) subject.teardown end it 'sets the reason for waking up' do expect(reasons).to receive(:<<).with(:teardown) subject.teardown end end end end listen-3.0.3/spec/lib/listen/event/processor_spec.rb000066400000000000000000000142251255413553300225220ustar00rootroot00000000000000require 'listen/event/processor' require 'listen/event/config' RSpec.describe Listen::Event::Processor do let(:event_queue) { instance_double(::Queue, 'event_queue') } let(:config) { instance_double(Listen::Event::Config) } let(:reasons) { instance_double(::Queue, 'reasons') } subject { described_class.new(config, reasons) } # This is to simulate events over various points in time let(:sequence) do {} end let(:state) do { time: 0 } end def status_for_time(time) # find the status of the listener for a given point in time previous_state_timestamps = sequence.keys.reject { |k| k > time } last_state_before_given_time = previous_state_timestamps.max sequence[last_state_before_given_time] end before do allow(config).to receive(:event_queue).and_return(event_queue) allow(config).to receive(:stopped?) do status_for_time(state[:time]) == :stopped end allow(config).to receive(:paused?) do status_for_time(state[:time]) == :paused end allow(config).to receive(:timestamp) do state[:time] end end describe '#loop_for' do before do allow(reasons).to receive(:empty?).and_return(true) end context 'when stopped' do before do sequence[0.0] = :stopped end context 'with pending changes' do before do allow(event_queue).to receive(:empty?).and_return(false) end it 'does not change the event queue' do subject.loop_for(1) end it 'does not sleep' do expect(config).to_not receive(:sleep) t = Time.now subject.loop_for(1) diff = Time.now.to_f - t.to_f expect(diff).to be < 0.01 end end end context 'when not stopped' do before do allow(event_queue).to receive(:empty?).and_return(true) end context 'when initially paused' do before do sequence[0.0] = :paused end context 'when stopped after sleeping' do before do sequence[0.2] = :stopped end it 'sleeps, waiting to be woken up' do expect(config).to receive(:sleep).once { state[:time] = 0.6 } subject.loop_for(1) end it 'breaks' do allow(config).to receive(:sleep).once { state[:time] = 0.6 } expect(config).to_not receive(:call) subject.loop_for(1) end end context 'when still paused after sleeping' do context 'when there were no events before' do before do sequence[1.0] = :stopped end it 'sleeps for latency to possibly later optimize some events' do # pretend we were woken up at 0.6 seconds since start allow(config).to receive(:sleep). with(no_args) { |*_args| state[:time] += 0.6 }.ordered # pretend we slept for latency (now: 1.6 seconds since start) allow(config).to receive(:sleep). with(1.0) { |*_args| state[:time] += 1.0 }.ordered subject.loop_for(1) end end context 'when there were no events for ages' do before do sequence[3.5] = :stopped # in the future to break from the loop end it 'still does not process events because it is paused' do # pretend we were woken up at 0.6 seconds since start allow(config).to receive(:sleep). with(no_args) { |*_args| state[:time] += 2.0 }.ordered # second loop starts here (no sleep, coz recent events, but no # processing coz paused # pretend we were woken up at 3.6 seconds since start allow(config).to receive(:sleep). with(no_args) { |*_args| state[:time] += 3.0 }.ordered subject.loop_for(1) end end end end context 'when initially processing' do before do sequence[0.0] = :processing end context 'when event queue is empty' do before do allow(event_queue).to receive(:empty?).and_return(true) end context 'when stopped after sleeping' do before do sequence[0.2] = :stopped end it 'sleeps, waiting to be woken up' do expect(config).to receive(:sleep). once { |*_args| state[:time] = 0.6 } subject.loop_for(1) end it 'breaks' do allow(config).to receive(:sleep). once { |*_args| state[:time] = 0.6 } expect(config).to_not receive(:call) subject.loop_for(1) end end end context 'when event queue has events' do before do end context 'when there were events ages ago' do before do sequence[3.5] = :stopped # in the future to break from the loop end it 'processes events' do allow(event_queue).to receive(:empty?). and_return(false, false, true) # resets latency check expect(config).to receive(:callable?).and_return(true) change = [:file, :modified, 'foo', 'bar'] resulting_changes = { modified: ['foo'], added: [], removed: [] } allow(event_queue).to receive(:pop).and_return(change) allow(config).to receive(:optimize_changes).with([change]). and_return(resulting_changes) final_changes = [['foo'], [], []] allow(config).to receive(:call) do |*changes| state[:time] = 4.0 # stopped expect(changes).to eq(final_changes) end subject.instance_variable_set(:@first_unprocessed_event_time, -3) subject.loop_for(1) end end # context "when stopped after sleeping" do # it "breaks from the loop" do # pending "todo" # end # end end end end end end listen-3.0.3/spec/lib/listen/event/queue_spec.rb000066400000000000000000000073741255413553300216360ustar00rootroot00000000000000require 'listen/event/queue' # TODO: not part of listener really RSpec.describe Listen::Event::Queue do let(:queue) { instance_double(Thread::Queue, 'my queue') } let(:config) { instance_double(Listen::Event::Queue::Config) } let(:relative) { false } let(:block) { proc {} } subject { described_class.new(config, &block) } before do allow(config).to receive(:relative?).and_return(relative) allow(Thread::Queue).to receive(:new).and_return(queue) end describe '#empty?' do before do allow(queue).to receive(:empty?).and_return(empty) end context 'when empty' do let(:empty) { true } it { is_expected.to be_empty } end context 'when not empty' do let(:empty) { false } let(:watched_dir) { fake_path('watched_dir') } before do allow(queue).to receive(:empty?).and_return(false) end it { is_expected.to_not be_empty } end end describe '#pop' do before do allow(queue).to receive(:pop).and_return('foo') end context 'when empty' do let(:value) { 'foo' } it 'forward the call to the queue' do expect(subject.pop).to eq('foo') end end end describe '#<<' do let(:watched_dir) { fake_path('watched_dir') } before do allow(queue).to receive(:<<) end context 'when a block is given' do let(:calls) { [] } let(:block) { proc { calls << 'called!' } } it 'calls the provided block' do subject.<<([:file, :modified, watched_dir, 'foo', {}]) expect(calls).to eq(['called!']) end end context 'when no block is given' do let(:calls) { [] } let(:block) { nil } it 'calls the provided block' do subject.<<([:file, :modified, watched_dir, 'foo', {}]) expect(calls).to eq([]) end end context 'when relative option is true' do let(:relative) { true } context 'when watched dir is the current dir' do let(:options) { { relative: true, directories: Pathname.pwd } } let(:dir_rel_path) { fake_path('.') } let(:foo_rel_path) { fake_path('foo', exist?: true) } it 'registers relative paths' do allow(dir_rel_path).to receive(:+).with('foo') { foo_rel_path } allow(watched_dir).to receive(:relative_path_from). with(Pathname.pwd). and_return(dir_rel_path) expect(queue).to receive(:<<). with([:file, :modified, dir_rel_path, 'foo', {}]) subject.<<([:file, :modified, watched_dir, 'foo', {}]) end end context 'when watched dir is not the current dir' do let(:options) { { relative: true } } let(:dir_rel_path) { fake_path('..') } let(:foo_rel_path) { fake_path('../foo', exist?: true) } it 'registers relative path' do allow(watched_dir).to receive(:relative_path_from). with(Pathname.pwd). and_return(dir_rel_path) expect(queue).to receive(:<<). with([:file, :modified, dir_rel_path, 'foo', {}]) subject.<<([:file, :modified, watched_dir, 'foo', {}]) end end context 'when watched dir is on another drive' do let(:watched_dir) { fake_path('watched_dir', realpath: 'd:/foo') } let(:foo_rel_path) { fake_path('d:/foo', exist?: true) } it 'registers full path' do allow(watched_dir).to receive(:relative_path_from). with(Pathname.pwd). and_raise(ArgumentError) allow(watched_dir).to receive(:+).with('foo') { foo_rel_path } expect(queue).to receive(:<<). with([:file, :modified, watched_dir, 'foo', {}]) subject.<<([:file, :modified, watched_dir, 'foo', {}]) end end end end end listen-3.0.3/spec/lib/listen/file_spec.rb000066400000000000000000000164561255413553300203110ustar00rootroot00000000000000RSpec.describe Listen::File do let(:record) do instance_double( Listen::Record, root: '/foo/bar', file_data: record_data, add_dir: true, update_file: true, unset_path: true, ) end let(:path) { Pathname.pwd } let(:subject) { described_class.change(record, 'file.rb') } around { |example| fixtures { example.run } } before { allow(::File).to receive(:lstat) { fail 'Not stubbed!' } } describe '#change' do let(:expected_data) do { mtime: kind_of(Float), mode: kind_of(Integer) } end context 'with file record' do let(:record_mtime) { nil } let(:record_md5) { nil } let(:record_mode) { nil } let(:record_data) do { mtime: record_mtime, md5: record_md5, mode: record_mode } end context 'with non-existing file' do before { allow(::File).to receive(:lstat) { fail Errno::ENOENT } } it { is_expected.to eq(:removed) } it 'sets path in record' do expect(record).to receive(:unset_path).with('file.rb') subject end end context 'with existing file' do let(:stat_mtime) { Time.now.to_f - 1234.567 } let(:stat_ctime) { Time.now.to_f - 1234.567 } let(:stat_atime) { Time.now.to_f - 1234.567 } let(:stat_mode) { 0640 } let(:md5) { fail 'stub me (md5)' } let(:stat) do instance_double( File::Stat, mtime: stat_mtime, atime: stat_atime, ctime: stat_ctime, mode: stat_mode ) end before do allow(::File).to receive(:lstat) { stat } allow(Digest::MD5).to receive(:file) { double(:md5, digest: md5) } end context 'with different mode in record' do let(:record_mode) { 0722 } it { should be :modified } it 'sets path in record with expected data' do expect(record).to receive(:update_file). with('file.rb', expected_data) subject end end context 'with same mode in record' do let(:record_mode) { stat_mode } # e.g. file was overwritten by earlier copy context 'with earlier mtime than in record' do let(:record_mtime) { stat_mtime.to_f - 123.45 } it { should be :modified } it 'sets path in record with expected data' do expect(record).to receive(:update_file). with('file.rb', expected_data) subject end end context 'with later mtime than in record' do let(:record_mtime) { stat_mtime.to_f + 123.45 } it { should be :modified } it 'sets path in record with expected data' do expect(record).to receive(:update_file). with('file.rb', expected_data) subject end end context 'with indentical mtime in record' do let(:record_mtime) { stat_mtime.to_f } context 'with accurate stat times' do let(:stat_mtime) { Time.at(1_401_235_714.123) } let(:stat_atime) { Time.at(1_401_235_714.123) } let(:stat_ctime) { Time.at(1_401_235_714.123) } let(:record_mtime) { stat_mtime.to_f } it { should be_nil } end context 'with inaccurate stat times' do let(:stat_mtime) { Time.at(1_401_235_714.0) } let(:stat_atime) { Time.at(1_401_235_714.0) } let(:stat_ctime) { Time.at(1_401_235_714.0) } let(:record_mtime) { stat_mtime.to_f } context 'with real mtime barely not within last second' do before { allow(Time).to receive(:now) { now } } # NOTE: if real mtime is ???14.99, the # saved mtime is ???14.0 let(:now) { Time.at(1_401_235_716.00) } it { should be_nil } end context 'with real mtime barely within last second' do # NOTE: real mtime is in range (???14.0 .. ???14.999), # so saved mtime at ???14.0 means it could be # ???14.999, so ???15.999 could still be within 1 second # range let(:now) { Time.at(1_401_235_715.999999) } before { allow(Time).to receive(:now) { now } } context 'without available md5' do let(:md5) { fail Errno::ENOENT } # Treat it as a removed file, because chances are ... # whatever is listening for changes won't be able to deal # with the file either (e.g. because of permissions) it { should be :removed } it 'should not unset record' do expect(record).to_not receive(:unset_path) end end context 'with available md5' do let(:md5) { 'd41d8cd98f00b204e9800998ecf8427e' } context 'with same md5 in record' do let(:record_md5) { md5 } it { should be_nil } end context 'with no md5 in record' do let(:record_md5) { nil } it { should be_nil } end context 'with different md5 in record' do let(:record_md5) { 'foo' } it { should be :modified } it 'sets path in record with expected data' do expected = expected_data. merge(md5: md5) expect(record).to receive(:update_file). with('file.rb', expected) subject end end end end end end end end end context 'with empty record' do let(:record_data) { {} } context 'with existing path' do let(:stat) do instance_double( File::Stat, mtime: 1234, mode: 0645 ) end before do allow(::File).to receive(:lstat) { stat } end it 'returns added' do expect(subject).to eq :added end it 'sets path in record with expected data' do expect(record).to receive(:update_file). with('file.rb', expected_data) subject end end end end describe '#inaccurate_mac_time?' do let(:stat) do instance_double(File::Stat, mtime: mtime, atime: atime, ctime: ctime) end subject { Listen::File.inaccurate_mac_time?(stat) } context 'with no accurate times' do let(:mtime) { Time.at(1_234_567.0) } let(:atime) { Time.at(1_234_567.0) } let(:ctime) { Time.at(1_234_567.0) } it { should be_truthy } end context 'with all accurate times' do let(:mtime) { Time.at(1_234_567.89) } let(:atime) { Time.at(1_234_567.89) } let(:ctime) { Time.at(1_234_567.89) } it { should be_falsey } end context 'with one accurate time' do let(:mtime) { Time.at(1_234_567.0) } let(:atime) { Time.at(1_234_567.89) } let(:ctime) { Time.at(1_234_567.0) } it { should be_falsey } end end end listen-3.0.3/spec/lib/listen/listener/000077500000000000000000000000001255413553300176445ustar00rootroot00000000000000listen-3.0.3/spec/lib/listen/listener/config_spec.rb000066400000000000000000000013671255413553300224570ustar00rootroot00000000000000require 'listen/listener/config' RSpec.describe Listen::Listener::Config do describe 'options' do context 'custom options' do subject do described_class.new( latency: 1.234, wait_for_delay: 0.85, force_polling: true, relative: true) end it 'extracts adapter options' do klass = Class.new do DEFAULTS = { latency: 5.4321 } end expected = { latency: 1.234 } expect(subject.adapter_instance_options(klass)).to eq(expected) end it 'extract adapter selecting options' do expected = { force_polling: true, polling_fallback_message: nil } expect(subject.adapter_select_options).to eq(expected) end end end end listen-3.0.3/spec/lib/listen/listener_spec.rb000066400000000000000000000203451255413553300212070ustar00rootroot00000000000000include Listen RSpec.describe Listener do let(:realdir1) { fake_path('/foo/dir1', children: []) } let(:realdir2) { fake_path('/foo/dir2', children: []) } let(:dir1) { fake_path('dir1', realpath: realdir1) } let(:dir2) { fake_path('dir2', realpath: realdir2) } let(:dirs) { ['dir1'] } let(:block) { instance_double(Proc) } subject do described_class.new(*(dirs + [options]).compact) do |*changes| block.call(*changes) end end let(:options) { {} } let(:record) { instance_double(Record, build: true, root: 'dir2') } let(:silencer) { instance_double(Silencer, configure: nil) } let(:backend_class) { class_double('Listen::Backend') } let(:backend) { instance_double(Backend) } let(:optimizer_config) { instance_double(QueueOptimizer::Config) } let(:optimizer) { instance_double(QueueOptimizer) } let(:processor_config) { instance_double(Event::Config) } let(:processor) { instance_double(Event::Loop) } let(:event_queue) { instance_double(Event::Queue) } let(:default_latency) { 0.1 } let(:backend_wait_for_delay) { 0.123 } let(:processing_thread) { instance_double(Thread) } before do allow(Silencer).to receive(:new) { silencer } allow(Backend).to receive(:new). with(anything, event_queue, silencer, anything). and_return(backend) allow(backend).to receive(:min_delay_between_events). and_return(backend_wait_for_delay) # TODO: use a configuration object to clean this up allow(QueueOptimizer::Config).to receive(:new).with(backend, silencer). and_return(optimizer_config) allow(QueueOptimizer).to receive(:new).with(optimizer_config). and_return(optimizer) allow(Event::Queue).to receive(:new).and_return(event_queue) allow(Event::Config).to receive(:new). with(anything, event_queue, optimizer, backend_wait_for_delay). and_return(processor_config) allow(Event::Loop).to receive(:new).with(processor_config). and_return(processor) allow(Record).to receive(:new).and_return(record) allow(Pathname).to receive(:new).with('dir1').and_return(dir1) allow(Pathname).to receive(:new).with('dir2').and_return(dir2) allow(Internals::ThreadPool).to receive(:add).and_return(processing_thread) allow(processing_thread).to receive(:alive?).and_return(true) allow(processing_thread).to receive(:wakeup) allow(processing_thread).to receive(:join) allow(block).to receive(:call) end describe 'initialize' do it { should_not be_paused } context 'with a block' do let(:myblock) { instance_double(Proc) } let(:block) { proc { myblock.call } } subject { described_class.new('dir1', &block) } it 'passes the block to the event processor' do allow(Event::Config).to receive(:new) do |*_args, &some_block| expect(some_block).to be some_block.call processor_config end expect(myblock).to receive(:call) subject end end context 'with directories' do subject { described_class.new('dir1', 'dir2') } it 'passes directories to backend' do allow(Backend).to receive(:new). with(['dir1', 'dir2'], anything, anything, anything). and_return(backend) subject end end end describe '#start' do before do allow(backend).to receive(:start) allow(silencer).to receive(:silenced?) { false } end it 'sets paused to false' do allow(processor).to receive(:setup) allow(processor).to receive(:resume) subject.start expect(subject).to_not be_paused end it 'starts adapter' do expect(backend).to receive(:start) allow(processor).to receive(:setup) allow(processor).to receive(:resume) subject.start end end describe '#stop' do before do allow(backend).to receive(:start) allow(processor).to receive(:setup) allow(processor).to receive(:resume) subject.start end it 'terminates' do allow(backend).to receive(:stop) allow(processor).to receive(:teardown) subject.stop end end describe '#pause' do before do allow(backend).to receive(:start) allow(processor).to receive(:setup) allow(processor).to receive(:resume) subject.start end it 'sets paused to true' do allow(processor).to receive(:pause) subject.pause expect(subject).to be_paused end end describe 'unpause with start' do before do allow(backend).to receive(:start) allow(processor).to receive(:setup) allow(processor).to receive(:resume) subject.start allow(processor).to receive(:pause) subject.pause end it 'sets paused to false' do subject.start expect(subject).to_not be_paused end end describe '#paused?' do before do allow(backend).to receive(:start) allow(processor).to receive(:setup) allow(processor).to receive(:resume) subject.start end it 'returns true when paused' do allow(processor).to receive(:pause) subject.pause expect(subject).to be_paused end it 'returns false when not paused' do expect(subject).not_to be_paused end end describe '#listen?' do context 'when processing' do before do allow(backend).to receive(:start) allow(processor).to receive(:setup) allow(processor).to receive(:resume) subject.start end it { should be_processing } end context 'when stopped' do it { should_not be_processing } end context 'when paused' do before do allow(backend).to receive(:start) allow(processor).to receive(:setup) allow(processor).to receive(:resume) subject.start allow(processor).to receive(:pause) subject.pause end it { should_not be_processing } end end # TODO: move these to silencer_controller? describe '#ignore' do context 'with existing ignore options' do let(:options) { { ignore: /bar/ } } it 'adds up to existing ignore options' do expect(silencer).to receive(:configure).once.with(ignore: [/bar/]) subject expect(silencer).to receive(:configure).once. with(ignore: [/bar/, /foo/]) subject.ignore(/foo/) end end context 'with existing ignore options (array)' do let(:options) { { ignore: [/bar/] } } it 'adds up to existing ignore options' do expect(silencer).to receive(:configure).once.with(ignore: [/bar/]) subject expect(silencer).to receive(:configure).once. with(ignore: [/bar/, /foo/]) subject.ignore(/foo/) end end end # TODO: move these to silencer_controller? describe '#ignore!' do context 'with no existing options' do let(:options) { {} } it 'sets options' do expect(silencer).to receive(:configure).with(options) subject end end context 'with existing ignore! options' do let(:options) { { ignore!: /bar/ } } it 'overwrites existing ignore options' do expect(silencer).to receive(:configure).once.with(ignore!: [/bar/]) subject expect(silencer).to receive(:configure).once.with(ignore!: [/foo/]) subject.ignore!([/foo/]) end end context 'with existing ignore options' do let(:options) { { ignore: /bar/ } } it 'deletes ignore options' do expect(silencer).to receive(:configure).once.with(ignore: [/bar/]) subject expect(silencer).to receive(:configure).once.with(ignore!: [/foo/]) subject.ignore!([/foo/]) end end end describe '#only' do context 'with existing only options' do let(:options) { { only: /bar/ } } it 'overwrites existing ignore options' do expect(silencer).to receive(:configure).once.with(only: [/bar/]) subject expect(silencer).to receive(:configure).once.with(only: [/foo/]) subject.only([/foo/]) end end end describe 'processing changes' do before do allow(backend).to receive(:start) end end context 'when listener is stopped' do before do subject.stop allow(silencer).to receive(:silenced?) { true } end end end listen-3.0.3/spec/lib/listen/queue_optimizer_spec.rb000066400000000000000000000064001255413553300226040ustar00rootroot00000000000000RSpec.describe Listen::QueueOptimizer do let(:config) { instance_double(Listen::QueueOptimizer::Config) } subject { described_class.new(config) } # watched dir let(:dir) { fake_path('dir') } # files let(:foo) { fake_path('foo') } let(:bar) { fake_path('bar') } let(:ignored) { fake_path('ignored') } before do allow(config).to receive(:debug) allow(dir).to receive(:+).with('foo') { foo } allow(dir).to receive(:+).with('bar') { bar } allow(dir).to receive(:+).with('ignored') { ignored } allow(config).to receive(:silenced?). with(Pathname('ignored'), :file) { true } allow(config).to receive(:silenced?). with(Pathname('foo'), :file) { false } allow(config).to receive(:silenced?). with(Pathname('bar'), :file) { false } allow(config).to receive(:exist?).with(foo).and_return(true) allow(config).to receive(:exist?).with(bar).and_return(true) allow(config).to receive(:exist?).with(ignored).and_return(true) end describe 'smoosh_changes' do subject { described_class.new(config).smoosh_changes(changes) } context 'with rename from temp file' do let(:changes) do [ [:file, :modified, dir, 'foo'], [:file, :removed, dir, 'foo'], [:file, :added, dir, 'foo'], [:file, :modified, dir, 'foo'] ] end it { is_expected.to eq(modified: ['foo'], added: [], removed: []) } end context 'with a deteted temp file' do before { allow(config).to receive(:exist?).with(foo).and_return(false) } let(:changes) do [ [:file, :added, dir, 'foo'], [:file, :modified, dir, 'foo'], [:file, :removed, dir, 'foo'], [:file, :modified, dir, 'foo'] ] end it { is_expected.to eq(modified: [], added: [], removed: []) } end # e.g. "mv foo x && mv x foo" is like "touch foo" context 'when double move' do let(:changes) do [ [:file, :removed, dir, 'foo'], [:file, :added, dir, 'foo'] ] end it { is_expected.to eq(modified: ['foo'], added: [], removed: []) } end context 'with cookie' do context 'when single moved' do let(:changes) { [[:file, :moved_to, dir, 'foo', cookie: 4321]] } it { is_expected.to eq(modified: [], added: ['foo'], removed: []) } end context 'when related moved_to' do let(:changes) do [ [:file, :moved_from, dir, 'foo', cookie: 4321], [:file, :moved_to, dir, 'bar', cookie: 4321] ] end it { is_expected.to eq(modified: [], added: ['bar'], removed: []) } end # Scenario with workaround for editors using rename() context 'when related moved_to with ignored moved_from' do let(:changes) do [ [:file, :moved_from, dir, 'ignored', cookie: 4321], [:file, :moved_to, dir, 'foo', cookie: 4321] ] end it { is_expected.to eq(modified: ['foo'], added: [], removed: []) } end end context 'with no cookie' do context 'with ignored file' do let(:changes) { [[:file, :modified, dir, 'ignored']] } it { is_expected.to eq(modified: [], added: [], removed: []) } end end end end listen-3.0.3/spec/lib/listen/record_spec.rb000066400000000000000000000240741255413553300206430ustar00rootroot00000000000000RSpec.describe Listen::Record do let(:dir) { instance_double(Pathname, to_s: '/dir') } let(:record) { Listen::Record.new(dir) } def dir_entries_for(hash) hash.each do |dir, entries| allow(::Dir).to receive(:entries).with(dir) { entries } end end def real_directory(hash) dir_entries_for(hash) hash.each do |dir, _| realpath(dir) end end def file(path) allow(::Dir).to receive(:entries).with(path).and_raise(Errno::ENOTDIR) path end def lstat(path, stat = nil) stat ||= instance_double(::File::Stat, mtime: 2.3, mode: 0755) allow(::File).to receive(:lstat).with(path).and_return(stat) stat end def realpath(path) allow(::File).to receive(:realpath).with(path).and_return(path) path end def symlink(hash_or_dir) if String === hash_or_dir allow(::File).to receive(:realpath).with(hash_or_dir). and_return(hash_or_dir) else hash_or_dir.each do |dir, real_path| allow(::File).to receive(:realpath).with(dir).and_return(real_path) end end end def record_tree(record) record.instance_variable_get(:@tree) end describe '#update_file' do context 'with path in watched dir' do it 'sets path by spliting dirname and basename' do record.update_file('file.rb', mtime: 1.1) expect(record_tree(record)).to eq('file.rb' => { mtime: 1.1 }) end it 'sets path and keeps old data not overwritten' do record.update_file('file.rb', foo: 1, bar: 2) record.update_file('file.rb', foo: 3) watched_dir = record_tree(record) 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.update_file('path/file.rb', mtime: 1.1) expect(record_tree(record)['path']).to eq('file.rb' => { mtime: 1.1 }) end it 'sets path and keeps old data not overwritten' do record.update_file('path/file.rb', foo: 1, bar: 2) record.update_file('path/file.rb', foo: 3) file_data = record_tree(record)['path']['file.rb'] expect(file_data).to eq(foo: 3, bar: 2) end end end describe '#add_dir' do it 'sets itself when .' do record.add_dir('.') expect(record_tree(record)).to eq({}) end it 'sets itself when nil' do record.add_dir(nil) expect(record_tree(record)).to eq({}) end it 'sets itself when empty' do record.add_dir('') expect(record_tree(record)).to eq({}) end it 'correctly sets new directory data' do record.add_dir('path/subdir') expect(record_tree(record)).to eq('path/subdir' => {}) end it 'sets path and keeps old data not overwritten' do record.add_dir('path/subdir') record.update_file('path/subdir/file.rb', mtime: 1.1) record.add_dir('path/subdir') record.update_file('path/subdir/file2.rb', mtime: 1.2) record.add_dir('path/subdir') watched = record_tree(record) expect(watched.keys).to eq ['path/subdir'] expect(watched['path/subdir'].keys).to eq %w(file.rb file2.rb) subdir = watched['path/subdir'] expect(subdir['file.rb']).to eq(mtime: 1.1) expect(subdir['file2.rb']).to eq(mtime: 1.2) end end describe '#unset_path' do context 'within watched dir' do context 'when path is present' do before { record.update_file('file.rb', mtime: 1.1) } it 'unsets path' do record.unset_path('file.rb') expect(record_tree(record)).to eq({}) end end context 'when path not present' do it 'unsets path' do record.unset_path('file.rb') expect(record_tree(record)).to eq({}) end end end context 'within subdir' do context 'when path is present' do before { record.update_file('path/file.rb', mtime: 1.1) } it 'unsets path' do record.unset_path('path/file.rb') expect(record_tree(record)).to eq('path' => {}) end end context 'when path not present' do it 'unsets path' do record.unset_path('path/file.rb') expect(record_tree(record)).to eq({}) end end end end describe '#file_data' do context 'with path in watched dir' do context 'when path is present' do before { record.update_file('file.rb', mtime: 1.1) } it 'returns file data' do expect(record.file_data('file.rb')).to eq(mtime: 1.1) end end context 'path not present' do it 'return empty hash' do expect(record.file_data('file.rb')).to be_empty end end end context 'with path in subdir' do context 'when path is present' do before { record.update_file('path/file.rb', mtime: 1.1) } it 'returns file data' do expected = { mtime: 1.1 } expect(record.file_data('path/file.rb')).to eq expected end end context 'path not present' do it 'return empty hash' do expect(record.file_data('path/file.rb')).to be_empty end end end end describe '#dir_entries' do context 'in watched dir' do subject { record.dir_entries('.') } context 'with no entries' do it { should be_empty } end context 'with file.rb in record' do before { record.update_file('file.rb', mtime: 1.1) } it { should eq('file.rb' => { mtime: 1.1 }) } end context 'with subdir/file.rb in record' do before { record.update_file('subdir/file.rb', mtime: 1.1) } it { should eq('subdir' => {}) } end end context 'in subdir /path' do subject { record.dir_entries('path') } context 'with no entries' do it { should be_empty } end context 'with path/file.rb already in record' do before { record.update_file('path/file.rb', mtime: 1.1) } it { should eq('file.rb' => { mtime: 1.1 }) } end end end describe '#build' do let(:dir1) { Pathname('/dir1') } before do stubs = { ::File => %w(lstat realpath), ::Dir => %w(entries exist?) } stubs.each do |klass, meths| meths.each do |meth| allow(klass).to receive(meth.to_sym) do |*args| fail "stub called: #{klass}.#{meth}(#{args.map(&:inspect) * ', '})" end end end end it 're-inits paths' do real_directory('/dir1' => []) real_directory('/dir' => []) record.update_file('path/file.rb', mtime: 1.1) record.build expect(record_tree(record)).to eq({}) expect(record.file_data('path/file.rb')).to be_empty end let(:foo_stat) { instance_double(::File::Stat, mtime: 1.0, mode: 0644) } let(:bar_stat) { instance_double(::File::Stat, mtime: 2.3, mode: 0755) } context 'with no subdirs' do before do real_directory('/dir' => %w(foo bar)) lstat(file('/dir/foo'), foo_stat) lstat(file('/dir/bar'), bar_stat) real_directory('/dir2' => []) end it 'builds record' do record.build expect(record_tree(record)). to eq( 'foo' => { mtime: 1.0, mode: 0644 }, 'bar' => { mtime: 2.3, mode: 0755 }) end end context 'with subdir containing files' do before do real_directory('/dir' => %w(dir1 dir2)) real_directory('/dir/dir1' => %w(foo)) real_directory('/dir/dir1/foo' => %w(bar)) lstat(file('/dir/dir1/foo/bar')) real_directory('/dir/dir2' => []) end it 'builds record' do record.build expect(record_tree(record)). to eq( 'dir1' => {}, 'dir1/foo' => { 'bar' => { mtime: 2.3, mode: 0755 } }, 'dir2' => {}, ) end end context 'with subdir containing dirs' do before do real_directory('/dir' => %w(dir1 dir2)) real_directory('/dir/dir1' => %w(foo)) real_directory('/dir/dir1/foo' => %w(bar baz)) real_directory('/dir/dir1/foo/bar' => []) real_directory('/dir/dir1/foo/baz' => []) real_directory('/dir/dir2' => []) allow(::File).to receive(:realpath) { |path| path } end it 'builds record' do record.build expect(record_tree(record)). to eq( 'dir1' => {}, 'dir1/foo' => {}, 'dir1/foo/bar' => {}, 'dir1/foo/baz' => {}, 'dir2' => {}, ) end end context 'with subdir containing symlink to parent' do subject { record.paths } before do real_directory('/dir' => %w(dir1 dir2)) real_directory('/dir/dir1' => %w(foo)) dir_entries_for('/dir/dir1/foo' => %w(dir1)) symlink('/dir/dir1/foo' => '/dir/dir1') real_directory('/dir/dir2' => []) end it 'shows a warning' do expect(STDERR).to receive(:puts). with(/directory is already being watched/) record.build # expect { record.build }. # to raise_error(RuntimeError, /Failed due to looped symlinks/) end end context 'with a normal symlinked directory to another' do subject { record.paths } before do real_directory('/dir' => %w(dir1)) real_directory('/dir/dir1' => %w(foo)) symlink('/dir/dir1/foo' => '/dir/dir2') dir_entries_for('/dir/dir1/foo' => %w(bar)) lstat(realpath(file('/dir/dir1/foo/bar'))) real_directory('/dir/dir2' => %w(bar)) lstat(file('/dir/dir2/bar')) end it 'shows message' do expect(STDERR).to_not receive(:puts) record.build end end context 'with subdir containing symlinked file' do subject { record.paths } before do real_directory('/dir' => %w(dir1 dir2)) real_directory('/dir/dir1' => %w(foo)) lstat(file('/dir/dir1/foo')) real_directory('/dir/dir2' => []) end it 'shows a warning' do expect(STDERR).to_not receive(:puts) record.build end end end end listen-3.0.3/spec/lib/listen/silencer/000077500000000000000000000000001255413553300176235ustar00rootroot00000000000000listen-3.0.3/spec/lib/listen/silencer/controller_spec.rb000066400000000000000000000056161255413553300233550ustar00rootroot00000000000000require 'listen/silencer/controller' RSpec.describe Listen::Silencer::Controller do let(:silencer) { instance_double(Listen::Silencer) } describe 'append_ignores' do context 'with no previous :ignore rules' do subject do described_class.new(silencer, {}) end before do allow(silencer).to receive(:configure).with({}) end context 'when providing a nil' do it 'sets the given :ignore rules as empty array' do subject allow(silencer).to receive(:configure).with(ignore: []) subject.append_ignores(nil) end end context 'when providing a single regexp as argument' do it 'sets the given :ignore rules as array' do subject allow(silencer).to receive(:configure).with(ignore: [/foo/]) subject.append_ignores(/foo/) end end context 'when providing multiple arguments' do it 'sets the given :ignore rules as a flat array' do subject allow(silencer).to receive(:configure).with(ignore: [/foo/, /bar/]) subject.append_ignores(/foo/, /bar/) end end context 'when providing as array' do it 'sets the given :ignore rules' do subject allow(silencer).to receive(:configure).with(ignore: [/foo/, /bar/]) subject.append_ignores([/foo/, /bar/]) end end end context 'with previous :ignore rules' do subject do described_class.new(silencer, ignore: [/foo/, /bar/]) end before do allow(silencer).to receive(:configure).with(ignore: [/foo/, /bar/]) end context 'when providing a nil' do # TODO: should this invocation maybe reset the rules? it 'reconfigures with existing :ignore rules' do subject allow(silencer).to receive(:configure).with(ignore: [/foo/, /bar/]) subject.append_ignores(nil) end end context 'when providing a single regexp as argument' do it 'appends the given :ignore rules as array' do subject expected = { ignore: [/foo/, /bar/, /baz/] } allow(silencer).to receive(:configure).with(expected) subject.append_ignores(/baz/) end end context 'when providing multiple arguments' do it 'appends the given :ignore rules as a flat array' do subject expected = { ignore: [/foo/, /bar/, /baz/, /bak/] } allow(silencer).to receive(:configure).with(expected) subject.append_ignores(/baz/, /bak/) end end context 'when providing as array' do it 'appends the given :ignore rules' do subject expected = { ignore: [/foo/, /bar/, /baz/, /bak/] } allow(silencer).to receive(:configure).with(expected) subject.append_ignores([/baz/, /bak/]) end end end end end listen-3.0.3/spec/lib/listen/silencer_spec.rb000066400000000000000000000054341255413553300211700ustar00rootroot00000000000000RSpec::Matchers.define :accept do |type, path| match { |actual| !actual.silenced?(Pathname(path), type) } end RSpec.describe Listen::Silencer do let(:options) { {} } before { subject.configure(options) } describe '#silenced?' do it { should accept(:file, Pathname('some_dir') + 'some_file.rb') } context 'with default ignore' do hidden_ignored = %w(.git .svn .hg .rbx .bundle) other_ignored = %w(bundle vendor/bundle log tmp vendor/ruby) (hidden_ignored + other_ignored).each do |dir| it { should_not accept(:dir, dir) } it { should accept(:dir, "#{dir}foo") } it { should accept(:dir, "foo#{dir}") } end ignored = %w(.DS_Store foo.tmp foo~) # Gedit swap files ignored += %w(.goutputstream-S3FBGX) # Kate editor swap files ignored += %w(foo.rbo54321.new foo.rbB22583.new foo.rb.kate-swp) # Intellij swap files ignored += %w(foo.rb___jb_bak___ foo.rb___jb_old___) # Vim swap files ignored += %w(foo.swp foo.swx foo.swpx 4913) # sed temp files ignored += %w(sedq7eVAR sed86w1kB) ignored.each do |path| it { should_not accept(:file, path) } end %w( foo.tmpl file.new file54321.new a.swf 14913 49131 sed_ABCDE sedabcdefg .sedq7eVAR foo.sedq7eVAR sedatives sediments sedan2014 ).each do |path| it { should accept(:file, path) } end end context 'when ignoring *.pid' do let(:options) { { ignore: /\.pid$/ } } it { should_not accept(:file, 'foo.pid') } end context 'when ignoring foo/bar* and *.pid' do let(:options) { { ignore: [/^foo\/bar/, /\.pid$/] } } it { should_not accept(:file, 'foo/bar/baz') } it { should_not accept(:file, 'foo.pid') } end context 'when ignoring only *.pid' do let(:options) { { ignore!: /\.pid$/ } } it { should_not accept(:file, 'foo.pid') } it { should accept(:file, '.git') } end context 'when accepting only *foo*' do let(:options) { { only: /foo/ } } it { should accept(:file, 'foo') } it { should_not accept(:file, 'bar') } end context 'when accepting only foo/* and *.txt' do let(:options) { { only: [/^foo\//, /\.txt$/] } } it { should accept(:file, 'foo/bar.rb') } it { should accept(:file, 'bar.txt') } it { should_not accept(:file, 'bar/baz.rb') } it { should_not accept(:file, 'bar.rb') } end context 'when accepting only *.pid' do context 'when ignoring bar*' do let(:options) { { only: /\.pid$/, ignore: /^bar/ } } it { should_not accept(:file, 'foo.rb') } it { should_not accept(:file, 'bar.pid') } it { should accept(:file, 'foo.pid') } end end end end listen-3.0.3/spec/lib/listen_spec.rb000066400000000000000000000010351255413553300173550ustar00rootroot00000000000000RSpec.describe Listen do let(:listener) { instance_double(Listen::Listener, stop: nil) } after do Listen.stop end describe '.to' do it 'initalizes listener' do expect(Listen::Listener).to receive(:new).with('/path') { listener } described_class.to('/path') end end describe '.stop' do it 'stops all listeners' do allow(Listen::Listener).to receive(:new).with('/path') { listener } expect(listener).to receive(:stop) described_class.to('/path') Listen.stop end end end listen-3.0.3/spec/spec_helper.rb000066400000000000000000000022431255413553300165720ustar00rootroot00000000000000# TODO: reduce requires everwhere and be more strict about it require 'listen' Listen.logger.level = Logger::WARN unless ENV['LISTEN_GEM_DEBUGGING'] require 'listen/internals/thread_pool' def ci? ENV['CI'] end if ci? require 'coveralls' Coveralls.wear! end Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration RSpec.configure do |config| config.order = :random config.filter_run focus: true config.run_all_when_everything_filtered = true # config.fail_fast = !ci? config.expect_with :rspec do |c| c.syntax = :expect end config.mock_with :rspec do |mocks| mocks.verify_doubled_constant_names = true mocks.verify_partial_doubles = true end config.disable_monkey_patching! end module SpecHelpers def fake_path(str, options = {}) instance_double(Pathname, str, { to_s: str }.merge(options)) end end RSpec.configure do |config| config.include SpecHelpers end Thread.abort_on_exception = true RSpec.configuration.before(:each) do Listen::Internals::ThreadPool.stop end RSpec.configuration.after(:each) do Listen::Internals::ThreadPool.stop end listen-3.0.3/spec/support/000077500000000000000000000000001255413553300154675ustar00rootroot00000000000000listen-3.0.3/spec/support/acceptance_helper.rb000066400000000000000000000204431255413553300214440ustar00rootroot00000000000000{ modification: :modified, addition: :added, removal: :removed, queued_modification: :modified, queued_addition: :added }.each do |description, type| RSpec::Matchers.define "process_#{description}_of".to_sym do |expected| match do |actual| # Use cases: # 1. reset the changes so they don't have leftovers # 2. keep the queue if we're testing for existing accumulated changes # if were testing the queue (e.g. after unpause), don't reset check_already_queued = /queued_/ =~ description reset_queue = !check_already_queued actual.listen(reset_queue) do change_fs(type, expected) unless check_already_queued end actual.changes[type].include? expected end failure_message do |actual| result = actual.changes.inspect "expected #{result} to include #{description} of #{expected}" end failure_message_when_negated do |actual| result = actual.changes.inspect "expected #{result} to not include #{description} of #{expected}" end end end def change_fs(type, path) case type when :modified unless File.exist?(path) fail "Bad test: cannot modify #{path.inspect} (it doesn't exist)" end # wait until full second, because this might be followed by a modification # event (which otherwise may not be detected every time) _sleep_until_next_second(Pathname.pwd) open(path, 'a') { |f| f.write('foo') } # separate it from upcoming modifications" _sleep_to_separate_events when :added if File.exist?(path) fail "Bad test: cannot add #{path.inspect} (it already exists)" end # wait until full second, because this might be followed by a modification # event (which otherwise may not be detected every time) _sleep_until_next_second(Pathname.pwd) open(path, 'w') { |f| f.write('foo') } # separate it from upcoming modifications" _sleep_to_separate_events when :removed unless File.exist?(path) fail "Bad test: cannot remove #{path.inspect} (it doesn't exist)" end File.unlink(path) else fail "bad test: unknown type: #{type.inspect}" end end # Used by change_fs() above so that the FS change (e.g. file created) happens # as close to the start of a new second (time) as possible. # # E.g. if file is created at 1234567.999 (unix time), it's mtime on some # filesystems is rounded, so it becomes 1234567.0, but if the change # notification happens a little while later, e.g. at 1234568.111, now the file # mtime and the current time in seconds are different (1234567 vs 1234568), and # so the MD5 test won't kick in (see file.rb) - the file will not be considered # for content checking (md5), so File.change will consider the file unmodified. # # This means, that if a file is added at 1234567.888 (and updated in Record), # and then its content is modified at 1234567.999, and checking for changes # happens at 1234568.111, the modification won't be detected. # (because Record mtime is 1234567.0, current FS mtime from stat() is the # same, and the checking happens in another second - 1234568). # # So basically, adding a file and detecting its later modification should all # happen within 1 second (which makes testing and debugging difficult). # def _sleep_until_next_second(path) Listen::File.inaccurate_mac_time?(path) t = Time.now diff = t.to_f - t.to_i sleep(1.05 - diff) end # Special class to only allow changes within a specific time window class TimedChanges attr_reader :changes def initialize # Set to non-nil, because changes can immediately come after unpausing # listener in an Rspec 'before()' block @changes = { modified: [], added: [], removed: [] } end def change_offset Time.now.to_f - @yield_time end def freeze_offset result = @freeze_time - @yield_time # Make an "almost zero" value more readable result < 1e-4 ? 1e-4 : result end # Allow changes only during specific time wine def allow_changes(reset_queue = true) @freeze_time = nil if reset_queue # Clear to prepare for collecting new FS events @changes = { modified: [], added: [], removed: [] } else # Since we're testing the queue and the listener callback is adding # changes to the same hash (e.g. after a pause), copy the existing data # to a new, unfrozen hash @changes = @changes.dup if @changes.frozen? @changes ||= { modified: [], added: [], removed: [] } end @yield_time = Time.now.to_f yield # Prevent recording changes after timeout @changes.freeze @freeze_time = Time.now.to_f end end # Conveniently wrap a Listener instance for testing class ListenerWrapper attr_reader :listener, :changes attr_accessor :lag def initialize(callback, paths, *args) # Lag depends mostly on wait_for_delay On Linux desktop, it's 0.06 - 0.11 # # On Travis it used to be > 0.5, but that was before broadcaster sent # changes immediately, so 0.2-0.4 might be enough for Travis, but we set it # to 0.8 (because 0.75 wasn't enough recently) # # The value should be 2-3 x wait_for_delay + time between fs operation and # notification, which for polling and FSEvent means the configured latency @lag = Float(ENV['LISTEN_TESTS_DEFAULT_LAG'] || 0.2) @paths = paths # Isolate collected changes between tests/listener instances @timed_changes = TimedChanges.new if callback @listener = Listen.send(*args) do |modified, added, removed| # Add changes to trigger frozen Hash error, making sure lag is enough _add_changes(:modified, modified, changes) _add_changes(:added, added, changes) _add_changes(:removed, removed, changes) unless callback == :track_changes callback.call(modified, added, removed) end end else @listener = Listen.send(*args) end end def changes @timed_changes.changes end def listen(reset_queue = true) # Give previous events time to be received, queued and processed # so they complete and don't interfere sleep lag @timed_changes.allow_changes(reset_queue) do yield # Polling sleep (default: 1s) backend = @listener.instance_variable_get(:@backend) adapter = backend.instance_variable_get(:@adapter) sleep(1.0) if adapter.is_a?(Listen::Adapter::Polling) # Lag should include: # 0.1s - 0.2s if the test needs Listener queue to be processed # 0.1s in case the system is busy # 0.1s - for celluloid overhead and scheduling sleep lag end # Keep this to detect a lag too small (changes during this sleep # will trigger "frozen hash" error caught below (and displaying timeout # details) sleep 1 changes end private def _add_changes(type, changes, dst) dst[type] += _relative_path(changes) dst[type].uniq! dst[type].sort! rescue RuntimeError => e raise unless e.message == "can't modify frozen Hash" # Show how by much the changes missed the timeout change_offset = @timed_changes.change_offset freeze_offset = @timed_changes.freeze_offset msg = "Changes took #{change_offset}s (allowed lag: #{freeze_offset})s" # Use STDERR (workaround for Celluloid, since it catches abort) STDERR.puts msg abort(msg) end def _relative_path(changes) changes.map do |change| unfrozen_copy = change.dup [@paths].flatten.each do |path| sub = path.sub(/\/$/, '').to_s unfrozen_copy.gsub!(/^#{sub}\//, '') end unfrozen_copy end end end def setup_listener(options, callback = nil) ListenerWrapper.new(callback, paths, :to, paths, options) end def setup_recipient(port, callback = nil) ListenerWrapper.new(callback, paths, :on, port) end def _sleep_to_separate_events # separate the events or Darwin and Polling # will detect only the :added event # # (This is because both use directory scanning # through Celluloid tasks, which may not kick in # time before the next filesystem change) # # The minimum for this is the time it takes between a syscall # changing the filesystem ... and ... an async # Listen::File.scan to finish comparing the file with the # Record # # This necessary for: # - Darwin Adapter # - Polling Adapter # - Linux Adapter in FSEvent emulation mode # - maybe Windows adapter (probably not) sleep 0.4 end listen-3.0.3/spec/support/fixtures_helper.rb000066400000000000000000000014431255413553300212260ustar00rootroot00000000000000require 'tmpdir' include FileUtils # Prepares temporary fixture-directories and # cleans them afterwards. # # @param [Fixnum] number_of_directories the number of fixture-directories to # make # # @yield [path1, path2, ...] the empty fixture-directories # @yieldparam [String] path the path to a fixture directory # def fixtures(number_of_directories = 1) current_pwd = Dir.pwd paths = 1.upto(number_of_directories).map { mk_fixture_tmp_dir } FileUtils.cd(paths.first) if number_of_directories == 1 yield(*paths) ensure FileUtils.cd current_pwd paths.map { |p| FileUtils.rm_rf(p) if File.exist?(p) } end def mk_fixture_tmp_dir timestamp = Time.now.to_f.to_s.sub('.', '') + rand(9999).to_s path = Pathname.pwd.join('spec', '.fixtures', timestamp).expand_path path.tap(&:mkpath) end listen-3.0.3/spec/support/platform_helper.rb000066400000000000000000000004061255413553300211770ustar00rootroot00000000000000def darwin? RbConfig::CONFIG['target_os'] =~ /darwin/i end def linux? RbConfig::CONFIG['target_os'] =~ /linux/i end def bsd? RbConfig::CONFIG['target_os'] =~ /bsd|dragonfly/i end def windows? RbConfig::CONFIG['target_os'] =~ /mswin|mingw|cygwin/i end listen-3.0.3/vendor/000077500000000000000000000000001255413553300143165ustar00rootroot00000000000000listen-3.0.3/vendor/hound/000077500000000000000000000000001255413553300154335ustar00rootroot00000000000000listen-3.0.3/vendor/hound/config/000077500000000000000000000000001255413553300167005ustar00rootroot00000000000000listen-3.0.3/vendor/hound/config/style_guides/000077500000000000000000000000001255413553300214005ustar00rootroot00000000000000listen-3.0.3/vendor/hound/config/style_guides/ruby.yml000066400000000000000000000056341255413553300231140ustar00rootroot00000000000000AllCops: Exclude: - db/schema.rb AccessorMethodName: Enabled: false ActionFilter: Enabled: false Alias: Enabled: false ArrayJoin: Enabled: false AsciiComments: Enabled: false AsciiIdentifiers: Enabled: false Attr: Enabled: false BlockNesting: Enabled: false CaseEquality: Enabled: false CharacterLiteral: Enabled: false ClassAndModuleChildren: Enabled: false ClassLength: Enabled: false ClassVars: Enabled: false Style/CollectionMethods: PreferredMethods: find: detect reduce: inject collect: map find_all: select ColonMethodCall: Enabled: false CommentAnnotation: Enabled: false CyclomaticComplexity: Enabled: false Delegate: Enabled: false DeprecatedHashMethods: Enabled: false Documentation: Enabled: false DotPosition: EnforcedStyle: trailing DoubleNegation: Enabled: false EachWithObject: Enabled: false EmptyLiteral: Enabled: false Encoding: Enabled: false EvenOdd: Enabled: false FileName: Enabled: false FlipFlop: Enabled: false FormatString: Enabled: false GlobalVars: Enabled: false GuardClause: Enabled: false IfUnlessModifier: Enabled: false IfWithSemicolon: Enabled: false InlineComment: Enabled: false Lambda: Enabled: false LambdaCall: Enabled: false LineEndConcatenation: Enabled: false LineLength: Max: 80 MethodLength: Enabled: false ModuleFunction: Enabled: false NegatedIf: Enabled: false NegatedWhile: Enabled: false Next: Enabled: false NilComparison: Enabled: false Not: Enabled: false NumericLiterals: Enabled: false OneLineConditional: Enabled: false OpMethod: Enabled: false ParameterLists: Enabled: false PercentLiteralDelimiters: Enabled: false PerlBackrefs: Enabled: false PredicateName: NamePrefixBlacklist: - is_ Proc: Enabled: false RaiseArgs: Enabled: false RegexpLiteral: Enabled: false SelfAssignment: Enabled: false SingleLineBlockParams: Enabled: false SingleLineMethods: Enabled: false SignalException: Enabled: false SpecialGlobalVars: Enabled: false StringLiterals: EnforcedStyle: double_quotes VariableInterpolation: Enabled: false TrailingComma: Enabled: false TrivialAccessors: Enabled: false VariableInterpolation: Enabled: false WhenThen: Enabled: false WhileUntilModifier: Enabled: false WordArray: Enabled: false # Lint AmbiguousOperator: Enabled: false AmbiguousRegexpLiteral: Enabled: false AssignmentInCondition: Enabled: false ConditionPosition: Enabled: false DeprecatedClassMethods: Enabled: false ElseLayout: Enabled: false HandleExceptions: Enabled: false InvalidCharacterLiteral: Enabled: false LiteralInCondition: Enabled: false LiteralInInterpolation: Enabled: false Loop: Enabled: false ParenthesesAsGroupedExpression: Enabled: false RequireParentheses: Enabled: false UnderscorePrefixedVariableName: Enabled: false Void: Enabled: false