pax_global_header00006660000000000000000000000064145664501500014520gustar00rootroot0000000000000052 comment=7b7a2f586d4363ec6e2e82c72732df92a1476d6e listen-3.9.0/000077500000000000000000000000001456645015000130275ustar00rootroot00000000000000listen-3.9.0/.github/000077500000000000000000000000001456645015000143675ustar00rootroot00000000000000listen-3.9.0/.github/release-drafter.yml000066400000000000000000000007461456645015000201660ustar00rootroot00000000000000name-template: "v$NEXT_PATCH_VERSION" tag-template: "v$NEXT_PATCH_VERSION" categories: - title: "⚠️ Breaking Changes" label: "⚠️ Breaking" - title: "✨ New Features" label: "✨ Feature" - title: "🐛 Bug Fixes" label: "🐛 Bug Fix" - title: "📚 Documentation" label: "📚 Docs" - title: "🏠 Housekeeping" label: "🏠 Housekeeping" change-template: "- $TITLE (#$NUMBER) @$AUTHOR" no-changes-template: "- No changes" template: | $CHANGES listen-3.9.0/.github/workflows/000077500000000000000000000000001456645015000164245ustar00rootroot00000000000000listen-3.9.0/.github/workflows/development.yml000066400000000000000000000021111456645015000214640ustar00rootroot00000000000000name: Development on: [push, pull_request] jobs: test: runs-on: ${{matrix.os}}-latest continue-on-error: ${{matrix.experimental}} strategy: fail-fast: false matrix: os: - ubuntu - macos ruby: - 2.7 - "3.0" # quotes ensure this isn't misinterpreted as Integer 3: https://github.com/actions/runner/issues/849 - 3.1 - 3.2 - 3.3 experimental: [false] env: [""] include: - os: macos ruby: truffleruby-head experimental: true - os: ubuntu ruby: truffleruby-head experimental: true - os: ubuntu ruby: jruby experimental: true - os: ubuntu ruby: head experimental: true steps: - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Run tests timeout-minutes: 6 run: | ${{matrix.env}} bundle exec rspec listen-3.9.0/.github/workflows/push.yml000066400000000000000000000004331456645015000201260ustar00rootroot00000000000000on: push name: Push jobs: draftRelease: name: Draft Release runs-on: ubuntu-latest steps: - uses: actions/checkout@master - name: Draft Release uses: toolmantim/release-drafter@v5.2.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} listen-3.9.0/.gitignore000066400000000000000000000003751456645015000150240ustar00rootroot00000000000000pkg/* 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.9.0/.hound.yml000066400000000000000000000001041456645015000147400ustar00rootroot00000000000000ruby: enabled: true version: 0.91.0 config_file: .rubocop.yml listen-3.9.0/.rspec000066400000000000000000000000651456645015000141450ustar00rootroot00000000000000--color --format documentation --require spec_helper listen-3.9.0/.rubocop.yml000066400000000000000000000133231456645015000153030ustar00rootroot00000000000000AllCops: TargetRubyVersion: 3.0.4 Exclude: - Gemfile - Guardfile - Rakefile - vendor/**/* UseCache: false Layout/AccessModifierIndentation: IndentationWidth: 2 Layout/BeginEndAlignment: Enabled: true Layout/DotPosition: Enabled: true EnforcedStyle: trailing Layout/EmptyLineAfterGuardClause: Enabled: false Layout/EmptyLinesAroundAttributeAccessor: Enabled: true Layout/EndAlignment: EnforcedStyleAlignWith: variable Layout/HashAlignment: Enabled: false Layout/HeredocIndentation: Enabled: true Layout/LineLength: Max: 150 Layout/MultilineMethodCallBraceLayout: Enabled: false Layout/MultilineMethodCallIndentation: EnforcedStyle: indented Layout/SpaceAroundMethodCallOperator: Enabled: true Layout/SpaceInsideBlockBraces: EnforcedStyleForEmptyBraces: space Lint/AmbiguousBlockAssociation: Enabled: false Lint/AmbiguousRegexpLiteral: Enabled: false Lint/AssignmentInCondition: Enabled: true AllowSafeAssignment: true Lint/BinaryOperatorWithIdenticalOperands: Enabled: true Lint/ConstantDefinitionInBlock: Enabled: true Exclude: - "**/*_spec.rb" Lint/DeprecatedOpenSSLConstant: Enabled: true Lint/DuplicateElsifCondition: Enabled: true Lint/DuplicateRequire: Enabled: true Lint/DuplicateRescueException: Enabled: true Lint/EmptyConditionalBody: Enabled: true Lint/EmptyFile: Enabled: true Lint/EmptyWhen: Enabled: false Lint/FloatComparison: Enabled: true Lint/IdentityComparison: Enabled: true Lint/LiteralInInterpolation: Enabled: false Lint/MissingSuper: Enabled: true Lint/MixedRegexpCaptureTypes: Enabled: false Lint/OutOfRangeRegexpRef: Enabled: true Lint/RaiseException: Enabled: true Lint/RedundantRequireStatement: Enabled: false Lint/SelfAssignment: Enabled: true Lint/StructNewOverride: Enabled: true Lint/SuppressedException: Enabled: false Lint/TopLevelReturnWithArgument: Enabled: true Lint/TrailingCommaInAttributeDeclaration: Enabled: true Lint/UnreachableLoop: Enabled: true Lint/UselessMethodDefinition: Enabled: true Lint/UselessTimes: Enabled: true Metrics/AbcSize: Enabled: false Metrics/BlockLength: Exclude: - Rakefile - "**/*.rake" - "**/*_test.rb" - "**/*_spec.rb" Metrics/CyclomaticComplexity: Max: 10 Metrics/MethodLength: Max: 15 Metrics/ModuleLength: CountComments: false Max: 200 Enabled: true Metrics/ParameterLists: Enabled: true Max: 6 Metrics/PerceivedComplexity: Max: 10 Naming/AccessorMethodName: Enabled: false Naming/FileName: Enabled: false Exclude: [] Naming/HeredocDelimiterNaming: Enabled: false Naming/PredicateName: Enabled: true NamePrefix: - is_ - has_ - have_ ForbiddenPrefixes: - is_ Exclude: - spec/**/* Naming/RescuedExceptionsVariableName: Enabled: false Naming/VariableNumber: Enabled: false Style/AccessorGrouping: Enabled: true Style/Alias: Enabled: false Style/AndOr: EnforcedStyle: conditionals Style/ArrayCoercion: Enabled: true Style/BisectedAttrAccessor: Enabled: true Style/CaseLikeIf: Enabled: true Style/ClassAndModuleChildren: Enabled: true Style/CollectionMethods: Enabled: true PreferredMethods: collect: map collect!: map! inject: reduce detect: find find_all: select length: size Style/CombinableLoops: Enabled: true Style/CommentedKeyword: Enabled: false Style/Documentation: Enabled: false Style/DoubleNegation: Enabled: false Style/EachWithObject: Enabled: false Style/EmptyMethod: Enabled: false Style/ExplicitBlockArgument: Enabled: true Style/ExponentialNotation: Enabled: true Style/FormatStringToken: Enabled: false Style/GlobalStdStream: Enabled: false Style/GuardClause: Enabled: false Style/HashAsLastArrayItem: Enabled: true Style/HashEachMethods: Enabled: true Style/HashLikeCase: Enabled: true Style/HashSyntax: EnforcedStyle: ruby19_no_mixed_keys Style/HashTransformKeys: Enabled: true Style/HashTransformValues: Enabled: true Style/IfUnlessModifier: Enabled: false Style/InlineComment: Enabled: false Style/KeywordParametersOrder: Enabled: true Style/Lambda: Enabled: false Style/ModuleFunction: Enabled: false Style/NegatedIf: Enabled: false Style/Next: Enabled: false Style/NumericLiteralPrefix: Enabled: false Style/NumericLiterals: MinDigits: 9 Style/NumericPredicate: Enabled: false Style/OneLineConditional: Enabled: false Style/OptionalBooleanParameter: Enabled: true Style/ParallelAssignment: Enabled: false Style/PercentLiteralDelimiters: Enabled: false Style/PreferredHashMethods: Enabled: false Style/RaiseArgs: Enabled: false EnforcedStyle: exploded Style/RedundantAssignment: Enabled: true Style/RedundantFetchBlock: Enabled: true Style/RedundantFileExtensionInRequire: Enabled: true Style/RedundantRegexpCharacterClass: Enabled: true Style/RedundantRegexpEscape: Enabled: true Style/RedundantSelfAssignment: Enabled: true Style/RegexpLiteral: Enabled: false Style/RescueStandardError: Enabled: false Style/Send: Enabled: false Style/SignalException: Enabled: false Style/SingleArgumentDig: Enabled: true Style/SingleLineBlockParams: Enabled: false Style/SingleLineMethods: Enabled: false AllowIfMethodIsEmpty: true Style/SlicingWithRange: Enabled: true Style/SoleNestedConditional: Enabled: true Style/StringConcatenation: Enabled: true Style/StringLiterals: Enabled: false Style/StringLiteralsInInterpolation: Enabled: true EnforcedStyle: single_quotes Style/SymbolArray: Enabled: false Style/TrailingCommaInArguments: Enabled: false Style/TrailingCommaInArrayLiteral: Enabled: false Style/TrailingCommaInHashLiteral: Enabled: false Style/TrailingUnderscoreVariable: Enabled: false Style/VariableInterpolation: Enabled: false Style/WhenThen: Enabled: false Style/WordArray: Enabled: false listen-3.9.0/.yardopts000066400000000000000000000002541456645015000146760ustar00rootroot00000000000000--title 'Listen Documentation' --readme README.md --markup markdown --markup-provider redcarpet --private --protected --output-dir ./doc lib/**/*.rb - CHANGELOG.md LICENSE listen-3.9.0/CHANGELOG.md000066400000000000000000000001151456645015000146350ustar00rootroot00000000000000# Moved to [GitHub releases](https://github.com/guard/listen/releases) page. listen-3.9.0/CONTRIBUTING.md000066400000000000000000000036761456645015000152740ustar00rootroot00000000000000Contribute to Listen =================== File an issue ------------- If you haven't already, first see [TROUBLESHOOTING](https://github.com/guard/listen/blob/master/README.md#Issues-and-Troubleshooting) for known issues, solutions and workarounds. You can report bugs and feature requests to [GitHub Issues](https://github.com/guard/listen/issues). **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/blob/master/README.md#Issues-and-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). * Please **do not change** the version number. The title of your PR will automatically be included in the release notes for the next version of the gem. A maintainer can add one of the following GitHub labels to the PR to automatically categorize it when the release notes are generated: - ⚠️ Breaking - ✨ Feature - 🐛 Bug Fix - 📚 Docs - 🏠 Housekeeping For questions please join us in our [Google group](http://groups.google.com/group/guard-dev) or on `#guard` (irc.freenode.net). listen-3.9.0/Gemfile000066400000000000000000000016101456645015000143200ustar00rootroot00000000000000# frozen_string_literal: true source '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 'coveralls' gem 'rake' gem 'rspec', '~> 3.3' end group :development do gem 'bundler' gem 'gems', require: false gem 'guard-rspec', require: false gem 'guard-rubocop' gem 'netrc', require: false gem 'octokit', require: false gem 'pry-rescue' gem 'rubocop', '0.91.0' gem 'yard', require: false end listen-3.9.0/Guardfile000066400000000000000000000020501456645015000146510ustar00rootroot00000000000000ignore(%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.9.0/LICENSE.txt000066400000000000000000000020711456645015000146520ustar00rootroot00000000000000Copyright (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.9.0/README.md000066400000000000000000000516311456645015000143140ustar00rootroot00000000000000# Listen The `listen` gem listens to file modifications and notifies you about the changes. [![Development Status](https://github.com/guard/listen/workflows/Development/badge.svg)](https://github.com/guard/listen/actions?workflow=Development) [![Gem Version](https://badge.fury.io/rb/listen.svg)](http://badge.fury.io/rb/listen) [![Code Climate](https://codeclimate.com/github/guard/listen.svg)](https://codeclimate.com/github/guard/listen) [![Coverage Status](https://coveralls.io/repos/guard/listen/badge.svg?branch=master)](https://coveralls.io/r/guard/listen) ## 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. * Continuous Integration: tested on selected Ruby environments via [Github Workflows](https://github.com/guard/listen/tree/master/.github/workflows). ## Issues / limitations * Limited support for symlinked directories ([#279](https://github.com/guard/listen/issues/279)): * Symlinks are always followed ([#25](https://github.com/guard/listen/issues/25)). * Symlinked directories pointing within a watched directory are not supported ([#273](https://github.com/guard/listen/pull/273). * No directory/adapter-specific configuration options. * Support for plugins planned for future. * TCP functionality was removed in `listen` [3.0.0](https://github.com/guard/listen/releases/tag/v3.0.0) ([#319](https://github.com/guard/listen/issues/319), [#218](https://github.com/guard/listen/issues/218)). There are plans to extract this feature to separate gems ([#258](https://github.com/guard/listen/issues/258)), until this is finished, you can use by locking the `listen` gem to version `'~> 2.10'`. * Some filesystems won't work without polling (VM/Vagrant Shared folders, NFS, Samba, sshfs, etc.). * Windows and \*BSD adapter aren't continuously and automatically tested. * OSX adapter has some performance limitations ([#342](https://github.com/guard/listen/issues/342)). * Listeners do not notify across forked processes, if you wish for multiple processes to receive change notifications you must [listen inside of each process](https://github.com/guard/listen/issues/398#issuecomment-223957952). Pull requests or help is very welcome for these. ## Install The simplest way to install `listen` is to use [Bundler](http://bundler.io). ```ruby gem 'listen' ``` ## Complete Example Here is a complete example of using the `listen` gem: ```ruby require 'listen' listener = Listen.to('/srv/app') do |modified, added, removed| puts(modified: modified, added: added, removed: removed) end listener.start sleep ``` Running the above in the background, you can see the callback block being called in response to each command: ``` $ cd /srv/app $ touch a.txt {:modified=>[], :added=>["/srv/app/a.txt"], :removed=>[]} $ echo more >> a.txt {:modified=>["/srv/app/a.txt"], :added=>[], :removed=>[]} $ mv a.txt b.txt {:modified=>[], :added=>["/srv/app/b.txt"], :removed=>["/srv/app/a.txt"]} $ vi b.txt # add a line to this new file and press ZZ to save and exit {:modified=>["/srv/app/b.txt"], :added=>[], :removed=>[]} $ vi c.txt # add a line and press ZZ to save and exit {:modified=>[], :added=>["/srv/app/c.txt"], :removed=>[]} $ rm b.txt c.txt {:modified=>[], :added=>[], :removed=>["/srv/app/b.txt", "/srv/app/c.txt"]} ``` ## Usage Call `Listen.to` with one or more directories and the "changes" callback passed as a block. ``` ruby listener = Listen.to('dir/to/listen', 'dir/to/listen2') do |modified, added, removed| puts "modified absolute path array: #{modified}" puts "added absolute path array: #{added}" puts "removed absolute path array: #{removed}" end listener.start # starts a listener thread--does not block # do whatever you want here...just don't exit the process :) sleep ``` ## Changes Callback Changes to the listened-to directories are reported by the listener thread in a callback. The callback receives **three** array parameters: `modified`, `added` and `removed`, in that order. Each of these three is always an array with 0 or more entries. Each array entry is an absolute path. ### Pause / start / stop Listeners can also be easily paused and later un-paused with start: ``` ruby listener = Listen.to('dir/path/to/listen') { |modified, added, removed| puts 'handle changes here...' } 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.start # resumes processing changes 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_FILES 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: `:ignore` regexp patterns are evaluated against relative paths. Note: Ignoring paths does not improve performance, except when Polling ([#274](https://github.com/guard/listen/issues/274)). ### Only `Listen` watches all files (less the ignored ones) by default. If you want to only listen to a specific type of file (i.e., 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. ## 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_FILES 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 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." ``` ## Logging and Debugging `Listen` logs its activity to `Listen.logger`. This is the primary method of debugging. ### Custom Logger You can call `Listen.logger =` to set a custom `listen` logger for the process. For example: ``` ruby Listen.logger = Rails.logger ``` ### Default Logger If no custom logger is set, a default `listen` logger which logs to to `STDERR` will be created and assigned to `Listen.logger`. The default logger defaults to the `error` logging level (severity). You can override the logging level by setting the environment variable `LISTEN_GEM_DEBUGGING=`. For ``, all standard `::Logger` levels are supported, with any mix of upper-/lower-case: ``` ruby export LISTEN_GEM_DEBUGGING=debug # or 2 [deprecated] export LISTEN_GEM_DEBUGGING=info # or 1 or true or yes [deprecated] export LISTEN_GEM_DEBUGGING=warn export LISTEN_GEM_DEBUGGING=fatal export LISTEN_GEM_DEBUGGING=error ``` The default of `error` will be used if an unsupported value is set. Note: The alternate values `1`, `2`, `true` and `yes` shown above are deprecated and will be removed from `listen` v4.0. ### Disabling Logging If you want to disable `listen` logging, set ``` ruby Listen.logger = ::Logger.new('/dev/null') ``` ### Adapter Warnings If listen is having trouble with the underlying adapter, it will display warnings with `Kernel#warn` by default, which in turn writes to STDERR. Sometimes this is not desirable, for example in an environment where STDERR is ignored. For these reasons, the behavior can be configured using `Listen.adapter_warn_behavior =`: ``` ruby Listen.adapter_warn_behavior = :warn # default (true means the same) Listen.adapter_warn_behavior = :log # send to logger.warn Listen.adapter_warn_behavior = :silent # suppress all adapter warnings (nil or false mean the same) ``` Also there are some cases where specific warnings are not helpful. For example, if you are using the polling adapter--and expect to--you can suppress the warning about it by providing a callable object like a lambda or proc that determines the behavior based on the `message`: ``` ruby Listen.adapter_warn_behavior = ->(message) do case message when /Listen will be polling for changes/ :silent when /directory is already being watched/ :log else :warn end end ``` In cases where the `Listen` gem is embedded inside another service--such as `guard`--the above configuration can be set in the environment variable `LISTEN_GEM_ADAPTER_WARN_BEHAVIOR=warn|log|silent`. ## 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 you 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', platforms: [:mingw, :mswin, :x64_mingw, :jruby] ``` ### On \*BSD If you 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)? If you see: ``` Listen will be polling for changes. ``` This means the Listen gem can’t find an optimized adapter. Typically this is caused by: - You’re on Windows and WDM gem isn’t installed. - You’re running the app without Bundler or RubyGems. - Using Sass which includes an ancient (the “dinosaur” type of ancient) version of the Listen gem. Possible solutions: 1. Suppress the message by using the :force_polling option. Or, you could just ignore the message since it’s harmless. 2. Windows users: Install the WDM gem. 3. Upgrade Ruby (use RubyInstaller for Windows or RVM/rbenv for Mac) and RubyGems. 3. Run your apps using Bundler. 4. Sass users: Install the latest version of Listen and try again. #### Simplified Bundler and Sass example Create a Gemfile with these lines: ``` source 'https://rubygems.org' gem 'listen' gem 'sass' ``` Next, use Bundler to update gems: ``` $ bundle update $ bundle exec sass --watch # ... or whatever app is using Listen. ``` ### Increasing the amount of inotify watchers If you are running Debian, RedHat, or another similar Linux distribution, run the following in a terminal: ``` $ sudo sh -c "echo fs.inotify.max_user_watches=524288 >> /etc/sysctl.conf" $ sudo sysctl -p ``` If you are running ArchLinux, search the `/etc/sysctl.d/` directory for config files with the setting: ``` $ grep -H -s "fs.inotify.max_user_watches" /etc/sysctl.d/* /etc/sysctl.d/40-max_user_watches.conf:fs.inotify.max_user_watches=100000 ``` Then change the setting in the file you found above to a higher value (see [here](https://www.archlinux.org/news/deprecation-of-etcsysctlconf/) for why): ``` $ sudo sh -c "echo fs.inotify.max_user_watches=524288 > /etc/sysctl.d/40-max-user-watches.conf" $ sudo sysctl --system ``` #### The technical details Listen uses `inotify` by default on Linux to monitor directories for changes. It's not uncommon to encounter a system limit on the number of files you can monitor. For example, Ubuntu Lucid's (64bit) `inotify` limit is set to 8192. You can get your current inotify file watch limit by executing: ``` $ cat /proc/sys/fs/inotify/max_user_watches ``` When this limit is not enough to monitor all files inside a directory, the limit must be increased for Listen to work properly. You can set a new limit temporarily with: ``` $ sudo sysctl fs.inotify.max_user_watches=524288 $ sudo sysctl -p ``` If you like to make your limit permanent, use: ``` $ sudo sh -c "echo fs.inotify.max_user_watches=524288 >> /etc/sysctl.conf" $ sudo sysctl -p ``` You may also need to pay attention to the values of `max_queued_events` and `max_user_instances` if Listen keeps on complaining. #### More info Man page for [inotify(7)](https://linux.die.net/man/7/inotify). Blog post: [limit of inotify](https://blog.sorah.jp/2012/01/24/inotify-limitation). ### Issues and Troubleshooting If the gem doesn't work as expected, start by setting `LISTEN_GEM_DEBUGGING=debug` or `LISTEN_GEM_DEBUGGING=info` as described above in [Logging and Debugging](#logging-and-debugging). *NOTE: without providing the output after setting the `LISTEN_GEM_DEBUGGING=debug` environment variable, it is usually impossible to guess why `listen` is not working as expected.* #### 3 steps before you start diagnosing problems These 3 steps will: - help quickly troubleshoot obscure problems (trust me, most of them are obscure) - help quickly identify the area of the problem (a full list is below) - help you get familiar with listen's diagnostic mode (it really comes in handy, trust me) - help you create relevant output before you submit an issue (so we can respond with answers instead of tons of questions) Step 1 - The most important option in Listen For effective troubleshooting set the `LISTEN_GEM_DEBUGGING=info` variable before starting `listen`. Step 2 - Verify polling works Polling has to work ... or something is really wrong (and we need to know that before anything else). (see force_polling option). After starting `listen`, you should see something like: ``` INFO -- : Record.build(): 0.06773114204406738 seconds ``` Step 3 - Trigger some changes directly without using editors or apps Make changes e.g. touch foo or echo "a" >> foo (for troubleshooting, avoid using an editor which could generate too many misleading events). You should see something like: ``` INFO -- : listen: raw changes: [[:added, "/home/me/foo"]] INFO -- : listen: final changes: {:modified=>[], :added=>["/home/me/foo"], :removed=>[]} ``` "raw changes" contains changes collected during the :wait_for_delay and :latency intervals, while "final changes" is what listen decided are relevant changes (for better editor support). ## 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=debug`) * 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=debug` can help discover the actual events and time they happened. ## Tips and Techniques - Watch only directories you're interested in. - Set your editor to save quickly (e.g. without backup files, without atomic-save) - Tweak the `:latency` and `:wait_for_delay` options until you get good results (see [options](#options)). - Add `:ignore` rules to silence all events you don't care about (reduces a lot of noise, especially if you use it on directories) ## 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). ## Releasing ### Prerequisites * You must have commit rights to the GitHub repository. * You must have push rights for rubygems.org. ### How to release 1. Run `bundle install` to make sure that you have all the gems necessary for testing and releasing. 2. **Ensure all tests are passing by running `bundle exec rake`.** 3. Determine which would be the correct next version number according to [semver](http://semver.org/). 4. Update the version in `./lib/listen/version.rb`. 5. Update the version in the Install section of `./README.md` (`gem 'listen', '~> X.Y'`). 6. Commit the version in a single commit, the message should be "Preparing vX.Y.Z" 7. Run `bundle exec rake release:full`; this will tag, push to GitHub, and publish to rubygems.org. 8. Update and publish the release notes on the [GitHub releases page](https://github.com/guard/listen/releases) if necessary ## 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][]. * [Natalie 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 [Natalie 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.9.0/Rakefile000066400000000000000000000074011456645015000144760ustar00rootroot00000000000000# frozen_string_literal: true require '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.9.0/bin/000077500000000000000000000000001456645015000135775ustar00rootroot00000000000000listen-3.9.0/bin/listen000077500000000000000000000003451456645015000150250ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true require 'listen' require 'listen/cli' if !defined?(JRUBY_VERSION) && Signal.list.keys.include?('INT') Signal.trap('INT') { Thread.new { Listen.stop } } end Listen::CLI.start listen-3.9.0/lib/000077500000000000000000000000001456645015000135755ustar00rootroot00000000000000listen-3.9.0/lib/listen.rb000066400000000000000000000024471456645015000154270ustar00rootroot00000000000000# frozen_string_literal: true require 'logger' require 'weakref' require 'listen/logger' require 'listen/listener' # 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 @listeners = Queue.new 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) Listener.new(*args, &block).tap do |listener| @listeners.enq(WeakRef.new(listener)) end end # This is used by the `listen` binary to handle Ctrl-C # def stop while (listener = @listeners.deq(true)) begin listener.stop rescue WeakRef::RefError end end rescue ThreadError end end end listen-3.9.0/lib/listen/000077500000000000000000000000001456645015000150735ustar00rootroot00000000000000listen-3.9.0/lib/listen/adapter.rb000066400000000000000000000025341456645015000170440ustar00rootroot00000000000000# frozen_string_literal: true require '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].freeze POLLING_FALLBACK_MESSAGE = 'Listen will be polling for changes.'\ 'Learn more at https://github.com/guard/listen#listen-adapters.' class << self def select(options = {}) Listen.logger.debug 'Adapter: considering polling ...' return Polling if options[:force_polling] Listen.logger.debug 'Adapter: considering optimized backend...' return _usable_adapter_class if _usable_adapter_class Listen.logger.debug 'Adapter: falling back to polling...' _warn_polling_fallback(options) Polling rescue Listen.logger.warn format('Adapter: failed: %s:%s', $ERROR_POSITION.inspect, $ERROR_POSITION * "\n") raise end private def _usable_adapter_class OPTIMIZED_ADAPTERS.find(&:usable?) end def _warn_polling_fallback(options) msg = options.fetch(:polling_fallback_message, POLLING_FALLBACK_MESSAGE) Listen.adapter_warn("[Listen warning]:\n #{msg}") if msg end end end end listen-3.9.0/lib/listen/adapter/000077500000000000000000000000001456645015000165135ustar00rootroot00000000000000listen-3.9.0/lib/listen/adapter/base.rb000066400000000000000000000062371456645015000177620ustar00rootroot00000000000000# frozen_string_literal: true require 'listen/options' require 'listen/record' require 'listen/change' require 'listen/thread' module Listen module Adapter class Base attr_reader :options, :config # TODO: only used by tests DEFAULTS = {}.freeze 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 # rubocop:disable Metrics/MethodLength def configure if @configured Listen.logger.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, config.silencer) snapshot = Change.new(change_config, record) @snapshots[dir] = snapshot end end # rubocop:enable Metrics/MethodLength def started? @started end def start configure if started? Listen.logger.warn('Adapter already started!') return end @started = true @run_thread = Listen::Thread.new("run_thread") do @snapshots.each_value do |snapshot| _timed('Record.build()') { snapshot.record.build } end _run end end def stop _stop config.queue.close # this causes queue.pop to return `nil` to the front-end end private def _stop @run_thread&.kill @run_thread = nil end def _timed(title) start = MonotonicTime.now yield diff = MonotonicTime.now - 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_exception(msg, caller_stack) formatted = format( msg, $ERROR_INFO, $ERROR_POSITION * "\n", caller_stack * "\n" ) Listen.logger.error(formatted) end class << self def usable? const_get('OS_REGEXP') =~ RbConfig::CONFIG['target_os'] end end end end end listen-3.9.0/lib/listen/adapter/bsd.rb000066400000000000000000000057271456645015000176230ustar00rootroot00000000000000# frozen_string_literal: true # 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.freeze DEFAULTS = { events: [ :delete, :write, :extend, :attrib, :rename # :link, :revoke ] }.freeze 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 Listen.adapter_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.find { |_, 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 Listen.logger.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.9.0/lib/listen/adapter/config.rb000066400000000000000000000014661456645015000203140ustar00rootroot00000000000000# frozen_string_literal: true require 'pathname' module Listen module Adapter class Config attr_reader :directories, :silencer, :queue, :adapter_options def initialize(directories, queue, silencer, adapter_options) # Default to current directory if no directories are supplied directories = [Dir.pwd] if directories.to_a.empty? # TODO: fix (flatten, array, compact?) @directories = directories.map do |directory| Pathname.new(directory.to_s).realpath end @directories.each do |pathname| unless pathname.directory? fail ArgumentError, "must be a directory: #{pathname}" end end @silencer = silencer @queue = queue @adapter_options = adapter_options end end end end listen-3.9.0/lib/listen/adapter/darwin.rb000066400000000000000000000044251456645015000203310ustar00rootroot00000000000000# frozen_string_literal: true require 'listen/thread' module Listen module Adapter # Adapter implementation for Mac OS X `FSEvents`. # class Darwin < Base OS_REGEXP = /darwin(?(1|2)\d+)/i.freeze # The default delay between checking for changes. DEFAULTS = { latency: 0.1 }.freeze INCOMPATIBLE_GEM_VERSION = <<-EOS.gsub(/^ {8}/, '') rb-fsevent > 0.9.4 no longer supports OS X 10.6 through 10.8. Please add the following to your Gemfile to avoid polling for changes: require 'rbconfig' if RbConfig::CONFIG['target_os'] =~ /darwin(1[0-3])/i gem 'rb-fsevent', '<= 0.9.4' end EOS def self.usable? version = RbConfig::CONFIG['target_os'][OS_REGEXP, :major_version] return false unless version return true if version.to_i >= 13 # darwin13 is OS X 10.9 require 'rb-fsevent' fsevent_version = Gem::Version.new(FSEvent::VERSION) return true if fsevent_version <= Gem::Version.new('0.9.4') Listen.adapter_warn(INCOMPATIBLE_GEM_VERSION) false end private def _configure(dir, &callback) @callbacks[dir] = callback end def _run require 'rb-fsevent' worker = FSEvent.new dirs_to_watch = @callbacks.keys.map(&:to_s) Listen.logger.info { "fsevent: watching: #{dirs_to_watch.inspect}" } worker.watch(dirs_to_watch, { latency: options.latency }, &method(:_process_changes)) @worker_thread = Listen::Thread.new("worker_thread") { worker.run } end def _process_changes(dirs) dirs.each do |dir| dir = Pathname.new(dir.sub(%r{/$}, '')) @callbacks.each do |watched_dir, callback| if watched_dir.eql?(dir) || Listen::Directory.ascendant_of?(watched_dir, dir) callback.call(dir) end end end end def _process_event(dir, path) Listen.logger.debug { "fsevent: processing path: #{path.inspect}" } # TODO: does this preserve symlinks? rel_path = path.relative_path_from(dir).to_s _queue_change(:dir, dir, rel_path, recursive: true) end def _stop @worker_thread&.kill super end end end end listen-3.9.0/lib/listen/adapter/linux.rb000066400000000000000000000061021456645015000201760ustar00rootroot00000000000000# frozen_string_literal: true module Listen module Adapter # @see https://github.com/nex3/rb-inotify class Linux < Base OS_REGEXP = /linux/i.freeze DEFAULTS = { events: [ :recursive, :attrib, :create, :modify, :delete, :move, :close_write ], wait_for_delay: 0.1 }.freeze private README_URL = 'https://github.com/guard/listen'\ '/blob/master/README.md#increasing-the-amount-of-inotify-watchers' def _configure(directory, &callback) require 'rb-inotify' @worker ||= ::INotify::Notifier.new @worker.watch(directory.to_s, *options.events, &callback) rescue Errno::ENOSPC raise ::Listen::Error::INotifyMaxWatchesExceeded, <<~EOS Unable to monitor directories for changes because iNotify max watches exceeded. See #{README_URL} . EOS end def _run @worker.run end # rubocop:disable Metrics/MethodLength 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 Listen.logger.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 end _queue_change(:dir, dir, rel_path, {}) 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 # rubocop:enable Metrics/MethodLength 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 def _stop @worker&.close super end end end end listen-3.9.0/lib/listen/adapter/polling.rb000066400000000000000000000020141456645015000205010ustar00rootroot00000000000000# frozen_string_literal: true module 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 = //.freeze # match every OS DEFAULTS = { latency: 1.0, wait_for_delay: 0.05 }.freeze private def _configure(_, &callback) @polling_callbacks ||= [] @polling_callbacks << callback end def _run loop do start = MonotonicTime.now @polling_callbacks.each do |callback| callback.call(nil) if (nap_time = options.latency - (MonotonicTime.now - start)) > 0 # TODO: warn if nap_time is negative (polling too slow) sleep(nap_time) end end end end def _process_event(dir, _) _queue_change(:dir, dir, '.', recursive: true) end end end end listen-3.9.0/lib/listen/adapter/windows.rb000066400000000000000000000053261456645015000205400ustar00rootroot00000000000000# frozen_string_literal: true module Listen module Adapter # Adapter implementation for Windows `wdm`. # class Windows < Base OS_REGEXP = /mswin|mingw|cygwin/i.freeze 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 Listen.logger.debug format('wdm - load failed: %s:%s', $ERROR_INFO, $ERROR_POSITION * "\n") Listen.adapter_warn(BUNDLER_DECLARE_GEM) false end private def _configure(dir) require 'wdm' Listen.logger.debug 'wdm - starting...' @worker ||= WDM::Monitor.new @worker.watch_recursively(dir.to_s, :files) do |change| yield([:file, change]) end @worker.watch_recursively(dir.to_s, :directories) do |change| yield([:dir, change]) end @worker.watch_recursively(dir.to_s, :attributes, :last_write) do |change| yield([:attr, change]) end end def _run @worker.run! end # rubocop:disable Metrics/MethodLength def _process_event(dir, event) Listen.logger.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 case change.type when :removed # TODO: check if watched dir? _queue_change(:dir, dir, Pathname(rel_path).dirname.to_s, {}) when :added _queue_change(:dir, dir, rel_path, {}) # 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 end # rubocop:enable Metrics/MethodLength def _change(type) { modified: [:modified, :attrib], # TODO: is attrib really passed? added: [:added, :renamed_new_file], removed: [:removed, :renamed_old_file] }.find do |change, types| types.include?(type) and break change end end end end end listen-3.9.0/lib/listen/backend.rb000066400000000000000000000020561456645015000170120ustar00rootroot00000000000000# frozen_string_literal: true require 'listen/adapter' require 'listen/adapter/base' require 'listen/adapter/config' require 'forwardable' # This class just aggregates configuration object to avoid Listener specs # from exploding with huge test setup blocks module Listen class Backend extend Forwardable 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 delegate start: :adapter delegate stop: :adapter attr_reader :min_delay_between_events private attr_reader :adapter end end listen-3.9.0/lib/listen/change.rb000066400000000000000000000035551456645015000166550ustar00rootroot00000000000000# frozen_string_literal: true require '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.) # rubocop:disable Metrics/MethodLength # rubocop:disable Metrics/CyclomaticComplexity # rubocop:disable Metrics/PerceivedComplexity 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) elsif type == :dir # NOTE: POSSIBLE RECURSION # TODO: fix - use a queue instead Directory.scan(self, rel_path, options) elsif (change = File.change(record, rel_path)) && !options[:silence] @config.queue(:file, change, watched_dir, rel_path) end end # rubocop:enable Metrics/MethodLength # rubocop:enable Metrics/CyclomaticComplexity # rubocop:enable Metrics/PerceivedComplexity end end listen-3.9.0/lib/listen/cli.rb000066400000000000000000000030751456645015000161740ustar00rootroot00000000000000# frozen_string_literal: true require '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: 'One or more directories 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, 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.9.0/lib/listen/directory.rb000066400000000000000000000057251456645015000174350ustar00rootroot00000000000000# frozen_string_literal: true require 'set' module Listen # TODO: refactor (turn it into a normal object, cache the stat, etc) class Directory # rubocop:disable Metrics/MethodLength 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(_children(path)) Listen.logger.debug do format('%s: %s(%s): %s -> %s', (options[:silence] ? 'Recording' : 'Scanning'), rel_path, options.inspect, previous.inspect, current.inspect) end begin current.each do |full_path| type = ::File.lstat(full_path.to_s).directory? ? :dir : :file item_rel_path = full_path.relative_path_from(dir).to_s _change(snapshot, type, item_rel_path, options) end rescue Errno::ENOENT # The directory changed meanwhile, so rescan it current = Set.new(_children(path)) retry 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 { format('scan DIED: %s:%s', $ERROR_INFO, $ERROR_POSITION * "\n") } raise end # rubocop:enable Metrics/MethodLength def self.ascendant_of?(base, other) other.ascend do |ascendant| break true if base == ascendant end 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._children(path) return path.children unless RUBY_ENGINE == 'jruby' # JRuby inconsistency workaround, see: # https://github.com/jruby/jruby/issues/3840 exists = path.exist? directory = path.directory? exists && !directory and raise Errno::ENOTDIR, path.to_s path.children end end end listen-3.9.0/lib/listen/error.rb000066400000000000000000000005371456645015000165560ustar00rootroot00000000000000# frozen_string_literal: true # Besides programming error exceptions like ArgumentError, # all public interface exceptions should be declared here and inherit from Listen::Error. module Listen class Error < RuntimeError class NotStarted < Error; end class SymlinkLoop < Error; end class INotifyMaxWatchesExceeded < Error; end end end listen-3.9.0/lib/listen/event/000077500000000000000000000000001456645015000162145ustar00rootroot00000000000000listen-3.9.0/lib/listen/event/config.rb000066400000000000000000000013671456645015000200150ustar00rootroot00000000000000# frozen_string_literal: true module Listen module Event class Config attr_reader :listener, :event_queue, :min_delay_between_events 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(seconds) Kernel.sleep(seconds) end def call(*args) @block&.call(*args) end def callable? @block end def optimize_changes(changes) @queue_optimizer.smoosh_changes(changes) end end end end listen-3.9.0/lib/listen/event/loop.rb000066400000000000000000000037161456645015000175210ustar00rootroot00000000000000# frozen_string_literal: true require 'thread' require 'timeout' require 'listen/event/processor' require 'listen/thread' require 'listen/error' module Listen module Event class Loop include Listen::FSM Error = ::Listen::Error NotStarted = ::Listen::Error::NotStarted # for backward compatibility start_state :pre_start state :pre_start state :starting state :started state :stopped def initialize(config) @config = config @wait_thread = nil @reasons = ::Queue.new initialize_fsm end def wakeup_on_event if started? && @wait_thread&.alive? _wakeup(:event) end end def started? state == :started end MAX_STARTUP_SECONDS = 5.0 # @raises Error::NotStarted if background thread hasn't started in MAX_STARTUP_SECONDS def start # TODO: use a Fiber instead? return unless state == :pre_start transition! :starting @wait_thread = Listen::Thread.new("wait_thread") do _process_changes end Listen.logger.debug("Waiting for processing to start...") wait_for_state(:started, timeout: MAX_STARTUP_SECONDS) or raise Error::NotStarted, "thread didn't start in #{MAX_STARTUP_SECONDS} seconds (in state: #{state.inspect})" Listen.logger.debug('Processing started.') end def pause # TODO: works? # fail NotImplementedError end def stop transition! :stopped @wait_thread&.join @wait_thread = nil end def stopped? state == :stopped end private def _process_changes processor = Event::Processor.new(@config, @reasons) transition! :started processor.loop_for(@config.min_delay_between_events) end def _wakeup(reason) @reasons << reason @wait_thread.wakeup end end end end listen-3.9.0/lib/listen/event/processor.rb000066400000000000000000000063371456645015000205710ustar00rootroot00000000000000# frozen_string_literal: true require 'listen/monotonic_time' module Listen module Event class Processor def initialize(config, reasons) @config = config @listener = config.listener @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 event = _wait_until_events _check_stopped _wait_until_events_calm_down _wait_until_no_longer_paused _process_changes(event) end rescue Stopped Listen.logger.debug('Processing stopped') end private class Stopped < RuntimeError end def _wait_until_events_calm_down loop do now = MonotonicTime.now # 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(diff) end end def _wait_until_no_longer_paused @listener.wait_for_state(*(Listener.states.keys - [:paused])) end def _check_stopped if @listener.stopped? _flush_wakeup_reasons raise Stopped end end def _sleep(seconds) _check_stopped config.sleep(seconds) _check_stopped _flush_wakeup_reasons do |reason| if reason == :event && !@listener.paused? _remember_time_of_first_unprocessed_event end end end def _remember_time_of_first_unprocessed_event @_remember_time_of_first_unprocessed_event ||= MonotonicTime.now end def _reset_no_unprocessed_events @_remember_time_of_first_unprocessed_event = nil end def _deadline @_remember_time_of_first_unprocessed_event + @latency end # blocks until event is popped # returns the event or `nil` when the event_queue is closed def _wait_until_events config.event_queue.pop.tap do |_event| @_remember_time_of_first_unprocessed_event ||= MonotonicTime.now end end def _flush_wakeup_reasons until @reasons.empty? reason = @reasons.pop yield reason if block_given? end end # for easier testing without sleep loop def _process_changes(event) _reset_no_unprocessed_events changes = [event] changes << config.event_queue.pop until config.event_queue.empty? return unless config.callable? hash = config.optimize_changes(changes) result = [hash[:modified], hash[:added], hash[:removed]] return if result.all?(&:empty?) block_start = MonotonicTime.now exception_note = " (exception)" ::Listen::Thread.rescue_and_log('_process_changes') do config.call(*result) exception_note = nil end Listen.logger.debug "Callback#{exception_note} took #{MonotonicTime.now - block_start} sec" end attr_reader :config end end end listen-3.9.0/lib/listen/event/queue.rb000066400000000000000000000022061456645015000176650ustar00rootroot00000000000000# frozen_string_literal: true require 'thread' require 'forwardable' module Listen module Event class Queue extend Forwardable class Config def initialize(relative) @relative = relative end def relative? @relative end end def initialize(config) @event_queue = ::Queue.new @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 = if @config.relative? _safe_relative_from_cwd(dir) else dir end @event_queue << [type, change, dir, path, options] end delegate empty?: :@event_queue delegate pop: :@event_queue delegate close: :@event_queue private def _safe_relative_from_cwd(dir) dir.relative_path_from(Pathname.pwd) rescue ArgumentError dir end end end end listen-3.9.0/lib/listen/file.rb000066400000000000000000000060311456645015000163370ustar00rootroot00000000000000# frozen_string_literal: true require 'digest' module Listen class File # rubocop:disable Metrics/MethodLength # rubocop:disable Metrics/CyclomaticComplexity # rubocop:disable Metrics/PerceivedComplexity 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, size: lstat.size } 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 if data[:size] != record_data[:size] record.update_file(rel_path, data) return :modified end return if /1|true/ =~ ENV['LISTEN_GEM_DISABLE_HASHING'] return unless 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 happened 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 sha = Digest::SHA256.file(path).digest record.update_file(rel_path, data.merge(sha: sha)) if record_data[:sha] && sha != record_data[:sha] :modified end rescue SystemCallError record.unset_path(rel_path) :removed rescue Listen.logger.debug "lstat failed for: #{rel_path} (#{$ERROR_INFO})" raise end # rubocop:enable Metrics/MethodLength # rubocop:enable Metrics/CyclomaticComplexity # rubocop:enable Metrics/PerceivedComplexity 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.9.0/lib/listen/fsm.rb000066400000000000000000000105251456645015000162100ustar00rootroot00000000000000# frozen_string_literal: true # Code copied from https://github.com/celluloid/celluloid-fsm require 'thread' module Listen module FSM # Included hook to extend class methods def self.included(klass) klass.send :extend, ClassMethods end module ClassMethods # Obtain or set the start state # Passing a state name sets the start state def start_state(new_start_state = nil) if new_start_state new_start_state.is_a?(Symbol) or raise ArgumentError, "state name must be a Symbol (got #{new_start_state.inspect})" @start_state = new_start_state else defined?(@start_state) or raise ArgumentError, "`start_state :` must be declared before `new`" @start_state end end # The valid states for this FSM, as a hash with state name symbols as keys and State objects as values. def states @states ||= {} end # Declare an FSM state and optionally provide a callback block to fire on state entry # Options: # * to: a state or array of states this state can transition to def state(state_name, to: nil, &block) state_name.is_a?(Symbol) or raise ArgumentError, "state name must be a Symbol (got #{state_name.inspect})" states[state_name] = State.new(state_name, to, &block) end end # Note: including classes must call initialize_fsm from their initialize method. def initialize_fsm @fsm_initialized = true @state = self.class.start_state @mutex = ::Mutex.new @state_changed = ::ConditionVariable.new end # Current state of the FSM, stored as a symbol attr_reader :state # checks for one of the given states to wait for # if not already, waits for a state change (up to timeout seconds--`nil` means infinite) # returns truthy iff the transition to one of the desired state has occurred def wait_for_state(*wait_for_states, timeout: nil) wait_for_states.each do |state| state.is_a?(Symbol) or raise ArgumentError, "states must be symbols (got #{state.inspect})" end @mutex.synchronize do if !wait_for_states.include?(@state) @state_changed.wait(@mutex, timeout) end wait_for_states.include?(@state) end end private def transition(new_state_name) new_state_name.is_a?(Symbol) or raise ArgumentError, "state name must be a Symbol (got #{new_state_name.inspect})" if (new_state = validate_and_sanitize_new_state(new_state_name)) transition_with_callbacks!(new_state) end end # Low-level, immediate state transition with no checks or callbacks. def transition!(new_state_name) new_state_name.is_a?(Symbol) or raise ArgumentError, "state name must be a Symbol (got #{new_state_name.inspect})" @fsm_initialized or raise ArgumentError, "FSM not initialized. You must call initialize_fsm from initialize!" @mutex.synchronize do yield if block_given? @state = new_state_name @state_changed.broadcast end end def validate_and_sanitize_new_state(new_state_name) return nil if @state == new_state_name if current_state && !current_state.valid_transition?(new_state_name) valid = current_state.transitions.map(&:to_s).join(', ') msg = "#{self.class} can't change state from '#{@state}' to '#{new_state_name}', only to: #{valid}" raise ArgumentError, msg end unless (new_state = self.class.states[new_state_name]) new_state_name == self.class.start_state or raise ArgumentError, "invalid state for #{self.class}: #{new_state_name}" end new_state end def transition_with_callbacks!(new_state) transition! new_state.name new_state.call(self) end def current_state self.class.states[@state] end class State attr_reader :name, :transitions def initialize(name, transitions, &block) @name = name @block = block @transitions = if transitions Array(transitions).map(&:to_sym) end end def call(obj) obj.instance_eval(&@block) if @block end def valid_transition?(new_state) # All transitions are allowed if none are expressly declared !@transitions || @transitions.include?(new_state.to_sym) end end end end listen-3.9.0/lib/listen/listener.rb000066400000000000000000000062561456645015000172560ustar00rootroot00000000000000# frozen_string_literal: true require '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 # # rubocop:disable Metrics/MethodLength 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) 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) initialize_fsm end # rubocop:enable Metrics/MethodLength start_state :initializing state :initializing, to: [:backend_started, :stopped] state :backend_started, to: [:processing_events, :stopped] do @backend.start end state :processing_events, to: [:paused, :stopped] do @processor.start end state :paused, to: [:processing_events, :stopped] do @processor.pause end state :stopped, to: [:backend_started] do @backend.stop # halt events ASAP @processor.stop end # Starts processing events and starts adapters # or resumes invoking callbacks if paused def start case state when :initializing transition :backend_started transition :processing_events when :paused transition :processing_events else raise ArgumentError, "cannot start from state #{state.inspect}" end 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 stopped? state == :stopped 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 end end listen-3.9.0/lib/listen/listener/000077500000000000000000000000001456645015000167205ustar00rootroot00000000000000listen-3.9.0/lib/listen/listener/config.rb000066400000000000000000000021661456645015000205170ustar00rootroot00000000000000# frozen_string_literal: true module 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 }.freeze 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 attr_reader :min_delay_between_events, :silencer_rules 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.9.0/lib/listen/logger.rb000066400000000000000000000026161456645015000167040ustar00rootroot00000000000000# frozen_string_literal: true module Listen @logger = nil # Listen.logger will always be present. # If you don't want logging, set Listen.logger = ::Logger.new('/dev/null', level: ::Logger::UNKNOWN) @adapter_warn_behavior = :warn class << self attr_writer :logger attr_accessor :adapter_warn_behavior def logger @logger ||= default_logger end def adapter_warn(message) case ENV['LISTEN_GEM_ADAPTER_WARN_BEHAVIOR']&.to_sym || adapter_warn_behavior_callback(message) when :log logger.warn(message) when :silent, nil, false # do nothing else # :warn warn(message) end end private def default_logger level = case ENV['LISTEN_GEM_DEBUGGING'].to_s when /debug|2/i ::Logger::DEBUG when /info|true|yes|1/i ::Logger::INFO when /warn/i ::Logger::WARN when /fatal/i ::Logger::FATAL else ::Logger::ERROR end ::Logger.new(STDERR, level: level) end def adapter_warn_behavior_callback(message) if adapter_warn_behavior.respond_to?(:call) case behavior = adapter_warn_behavior.call(message) when Symbol behavior when false, nil :silent else :warn end else adapter_warn_behavior end end end end listen-3.9.0/lib/listen/monotonic_time.rb000066400000000000000000000007061456645015000204460ustar00rootroot00000000000000# frozen_string_literal: true module Listen module MonotonicTime class << self if defined?(Process::CLOCK_MONOTONIC) def now Process.clock_gettime(Process::CLOCK_MONOTONIC) end elsif defined?(Process::CLOCK_MONOTONIC_RAW) def now Process.clock_gettime(Process::CLOCK_MONOTONIC_RAW) end else def now Time.now.to_f end end end end end listen-3.9.0/lib/listen/options.rb000066400000000000000000000013011456645015000171060ustar00rootroot00000000000000# frozen_string_literal: true module Listen class Options def initialize(opts, defaults) @options = {} given_options = opts.dup defaults.each_key do |key| @options[key] = given_options.delete(key) || defaults[key] end given_options.empty? or raise ArgumentError, "Unknown options: #{given_options.inspect}" end # rubocop:disable Lint/MissingSuper def respond_to_missing?(name, *_) @options.has_key?(name) end def method_missing(name, *_) respond_to_missing?(name) or raise NameError, "Bad option: #{name.inspect} (valid:#{@options.keys.inspect})" @options[name] end # rubocop:enable Lint/MissingSuper end end listen-3.9.0/lib/listen/queue_optimizer.rb000066400000000000000000000072171456645015000206550ustar00rootroot00000000000000# frozen_string_literal: true module 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.find { |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.flat_map do |_, changes| if (editor_modified = editor_modified?(changes)) [[:modified, *editor_modified]] 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 end def editor_modified?(changes) return unless changes.size == 2 from_type = from = nil to_type = 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 end end # Expect an ignored moved_from and non-ignored moved_to # to qualify as an "editor modify" if from && to && config.silenced?(Pathname(from), from_type) && !config.silenced?(Pathname(to), to_type) [to_dir, to] end end end end listen-3.9.0/lib/listen/record.rb000066400000000000000000000064301456645015000167010ustar00rootroot00000000000000# frozen_string_literal: true require '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, silencer) reset_tree @root = directory.to_s @silencer = silencer end def add_dir(rel_path) if !empty_dirname?(rel_path.to_s) @tree[rel_path.to_s] end 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 empty_dirname?(dirname) @tree[basename].dup else @tree[dirname][basename] ||= {} @tree[dirname][basename].dup end end def dir_entries(rel_path) rel_path_s = rel_path.to_s subtree = if empty_dirname?(rel_path_s) @tree else @tree[rel_path_s] end subtree.each_with_object({}) do |(key, values), result| # only return data for file entries inside the dir (which will each be sub-hashes) if values.is_a?(Hash) result[key] = values.has_key?(:mtime) ? values : {} end end end def build reset_tree # 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 empty_dirname?(dirname) dirname == '.' || dirname == '' end def reset_tree @tree = Hash.new { |h, k| h[k] = {} } end def _fast_update_file(dirname, basename, data) if empty_dirname?(dirname.to_s) @tree[basename] = @tree[basename].merge(data) else @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 empty_dirname?(dirname.to_s) if @tree.key?(basename) @tree.delete(basename) end elsif @tree.key?(dirname) @tree[dirname].delete(basename) end end def _fast_build_dir(remaining, symlink_detector) entry = remaining.pop return if @silencer.silenced?(entry.record_dir_key, :dir) 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.9.0/lib/listen/record/000077500000000000000000000000001456645015000163515ustar00rootroot00000000000000listen-3.9.0/lib/listen/record/entry.rb000066400000000000000000000033521456645015000200420ustar00rootroot00000000000000# frozen_string_literal: true module 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 = root @relative = relative @name = name end attr_reader :root, :relative, :name def children child_relative = _join (_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, size: lstat.size } 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 def _entries(dir) return Dir.entries(dir) unless RUBY_ENGINE == 'jruby' # JRuby inconsistency workaround, see: # https://github.com/jruby/jruby/issues/3840 exists = ::File.exist?(dir) directory = ::File.directory?(dir) return Dir.entries(dir) unless exists && !directory raise Errno::ENOTDIR, dir end end end end listen-3.9.0/lib/listen/record/symlink_detector.rb000066400000000000000000000021451456645015000222570ustar00rootroot00000000000000# frozen_string_literal: true require 'set' require 'listen/error' module Listen # @private api class Record class SymlinkDetector README_URL = 'https://github.com/guard/listen/blob/master/README.md' SYMLINK_LOOP_ERROR = <<-EOS ** ERROR: directory is already being watched! ** Directory: %s is already being watched through: %s MORE INFO: #{README_URL} EOS Error = ::Listen::Error # for backward compatibility def initialize @real_dirs = Set.new end def verify_unwatched!(entry) real_path = entry.real_path @real_dirs.add?(real_path) or _fail(entry.sys_path, real_path) end # Leaving this stub here since some warning work-arounds were referring to it. # Deprecated. Will be removed in Listen v4.0. def warn(message) Listen.adapter_warn(message) end private def _fail(symlinked, real_path) warn(format(SYMLINK_LOOP_ERROR, symlinked, real_path)) raise ::Listen::Error::SymlinkLoop, 'Failed due to looped symlinks' end end end end listen-3.9.0/lib/listen/silencer.rb000066400000000000000000000045121456645015000172260ustar00rootroot00000000000000# frozen_string_literal: true module Listen class Silencer # The default list of directories that get ignored. DEFAULT_IGNORED_FILES = %r{\A(?: \.git | \.svn | \.hg | \.rbx | \.bundle | bundle | vendor/bundle | log | tmp | vendor/ruby # emacs temp files | \#.+\# | \.\#.+ )(/|\z)}x.freeze # The default list of files that get ignored. DEFAULT_IGNORED_EXTENSIONS = %r{(?: # 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' | (?:\A 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} ) ) # Mutagen sync temporary files | \.mutagen-temporary.* # other files | \.DS_Store | \.tmp | ~ )\z}x.freeze # TODO: deprecate these mutators; use attr_reader instead attr_accessor :only_patterns, :ignore_patterns def initialize(**options) configure(options) end # TODO: deprecate this mutator def configure(options) @only_patterns = options[:only] ? Array(options[:only]) : nil @ignore_patterns = _init_ignores(options[:ignore], options[:ignore!]) end def silenced?(relative_path, type) path = relative_path.to_s # in case it is a Pathname _ignore?(path) || (only_patterns && type == :file && !_only?(path)) end private def _ignore?(path) ignore_patterns.any? { |pattern| path =~ pattern } end def _only?(path) only_patterns.any? { |pattern| path =~ pattern } end def _init_ignores(ignores, overrides) patterns = [] unless overrides patterns << DEFAULT_IGNORED_FILES patterns << DEFAULT_IGNORED_EXTENSIONS end patterns << ignores patterns << overrides patterns.compact.flatten end end end listen-3.9.0/lib/listen/silencer/000077500000000000000000000000001456645015000166775ustar00rootroot00000000000000listen-3.9.0/lib/listen/silencer/controller.rb000066400000000000000000000023201456645015000214040ustar00rootroot00000000000000# frozen_string_literal: true module 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.9.0/lib/listen/thread.rb000066400000000000000000000030661456645015000166740ustar00rootroot00000000000000# frozen_string_literal: true require 'thread' require_relative 'logger' module Listen module Thread class << self # Creates a new thread with the given name. # Any exceptions raised by the thread will be logged with the thread name and complete backtrace. # rubocop:disable Style/MultilineBlockChain def new(name, &block) thread_name = "listen-#{name}" caller_stack = caller ::Thread.new do rescue_and_log(thread_name, caller_stack: caller_stack, &block) end.tap do |thread| thread.name = thread_name end end # rubocop:enable Style/MultilineBlockChain def rescue_and_log(method_name, *args, caller_stack: nil) yield(*args) rescue => exception _log_exception(exception, method_name, caller_stack: caller_stack) end private def _log_exception(exception, thread_name, caller_stack: nil) complete_backtrace = if caller_stack [*exception.backtrace, "--- Thread.new ---", *caller_stack] else exception.backtrace end message = "Exception rescued in #{thread_name}:\n#{_exception_with_causes(exception)}\n#{complete_backtrace * "\n"}" Listen.logger.error(message) end def _exception_with_causes(exception) result = +"#{exception.class}: #{exception}" if exception.cause result << "\n" result << "--- Caused by: ---\n" result << _exception_with_causes(exception.cause) end result end end end end listen-3.9.0/lib/listen/version.rb000066400000000000000000000001051456645015000171010ustar00rootroot00000000000000# frozen_string_literal: true module Listen VERSION = '3.9.0' end listen-3.9.0/listen.gemspec000066400000000000000000000026321456645015000156750ustar00rootroot00000000000000# frozen_string_literal: true lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'listen/version' Gem::Specification.new do |gem| # rubocop:disable Metrics/BlockLength gem.name = 'listen' gem.version = Listen::VERSION gem.license = 'MIT' gem.author = 'Thibaud Guillaume-Gentil' gem.email = 'thibaud@thibaud.gg' gem.homepage = 'https://github.com/guard/listen' gem.summary = 'Listen to file modifications' gem.description = 'The Listen gem listens to file modifications and '\ 'notifies you about the changes. Works everywhere!' gem.metadata = { 'allowed_push_host' => 'https://rubygems.org', 'bug_tracker_uri' => "#{gem.homepage}/issues", 'changelog_uri' => "#{gem.homepage}/releases", 'documentation_uri' => "https://www.rubydoc.info/gems/listen/#{gem.version}", 'homepage_uri' => gem.homepage, 'source_code_uri' => "#{gem.homepage}/tree/v#{gem.version}" } gem.files = `git ls-files -z`.split("\x0").select do |f| %r{^(?:bin|lib)/} =~ f end + %w[CHANGELOG.md CONTRIBUTING.md LICENSE.txt README.md] gem.test_files = [] gem.executable = 'listen' gem.require_path = 'lib' gem.required_ruby_version = '>= 2.4.0' # rubocop:disable Gemspec/RequiredRubyVersion gem.add_dependency 'rb-fsevent', '~> 0.10', '>= 0.10.3' gem.add_dependency 'rb-inotify', '~> 0.9', '>= 0.9.10' end listen-3.9.0/spec/000077500000000000000000000000001456645015000137615ustar00rootroot00000000000000listen-3.9.0/spec/acceptance/000077500000000000000000000000001456645015000160475ustar00rootroot00000000000000listen-3.9.0/spec/acceptance/listen_spec.rb000066400000000000000000000241151456645015000207070ustar00rootroot00000000000000# frozen_string_literal: true 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) } context 'with normal start/stop' do 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 } } modes = case ENV['TEST_LISTEN_ADAPTER_MODES'] 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 'listens to subdirectory removed' do around do |example| mkdir_p 'dir1' mkdir_p 'dir1/subdir1' mkdir_p 'dir1/subdir1/subdir2' touch 'dir1/subdir1/file.rb' touch 'dir1/subdir1/subdir2/file.rb' example.run end it 'listen to the files of a removed directory' do expected = { modified: [], added: [], removed: %w[dir1/subdir1/file.rb dir1/subdir1/subdir2/file.rb] } expect(wrapper.listen do rm_rf 'dir1' end).to eq expected end end context 'listens to sub-subdirectory removed' do around do |example| mkdir_p 'dir1' mkdir_p 'dir1/subdir1' mkdir_p 'dir1/subdir1/subdir2' touch 'dir1/subdir1/file.rb' touch 'dir1/subdir1/subdir2/file.rb' example.run end it 'listen to the files of a removed directory' do expected = { modified: [], added: [], removed: %w[dir1/subdir1/file.rb dir1/subdir1/subdir2/file.rb] } expect(wrapper.listen do rm_rf 'dir1/subdir1' 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 context 'with paths set' do let(:paths) { Pathname.new(Dir.pwd) } it 'allows extra calls to stop' do wrapper.listener.stop wrapper.listener.stop end end end context 'when never started' do let(:paths) { Pathname.new(Dir.pwd) } it 'allows stop' do wrapper.listener.stop end it 'extra calls to stop' do wrapper.listener.stop wrapper.listener.stop wrapper.listener.stop end end end listen-3.9.0/spec/lib/000077500000000000000000000000001456645015000145275ustar00rootroot00000000000000listen-3.9.0/spec/lib/listen/000077500000000000000000000000001456645015000160255ustar00rootroot00000000000000listen-3.9.0/spec/lib/listen/adapter/000077500000000000000000000000001456645015000174455ustar00rootroot00000000000000listen-3.9.0/spec/lib/listen/adapter/base_spec.rb000066400000000000000000000051111456645015000217140ustar00rootroot00000000000000# frozen_string_literal: true RSpec.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(:thread) { instance_double(Thread, "thread") } 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(Thread).to receive(:new) do |&block| block.call allow(thread).to receive(:name=) thread end # Stuff that happens in configure() allow(Listen::Record).to receive(:new).with(dir1, silencer).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.9.0/spec/lib/listen/adapter/bsd_spec.rb000066400000000000000000000003441456645015000215550ustar00rootroot00000000000000# frozen_string_literal: true RSpec.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.9.0/spec/lib/listen/adapter/config_spec.rb000066400000000000000000000075431456645015000222620ustar00rootroot00000000000000# frozen_string_literal: true require '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(:path3) { fake_path('/real/path3', realpath: real_path3) } let(:current_path) do fake_path('/real/current_path', realpath: real_current_path) end 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') } let(:real_path3) { fake_path('/real/path3', directory?: false) } let(:real_current_path) { fake_path('/real/current_path') } 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('/real/path3').and_return(path3) 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) allow(Dir).to receive(:pwd).and_return('/real/current_path') allow(Pathname).to receive(:new). with('/real/current_path').and_return(current_path) 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) { %w[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 let(:directories) { } it 'returns the current path in array' do expect(subject.directories).to eq([real_current_path]) end end context 'with file path' do let(:directories) { ['/real/path3'] } it 'raises argument error requesting a directory' do expect { subject }.to raise_error(ArgumentError, /must be a directory/) end 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.9.0/spec/lib/listen/adapter/darwin_spec.rb000066400000000000000000000041211456645015000222660ustar00rootroot00000000000000# frozen_string_literal: true # 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 } context 'on darwin 13.0 (OS X Mavericks)' do before do allow(RbConfig::CONFIG).to receive(:[]).and_return('darwin13.0') end it { should be_usable } end context 'on darwin20 (macOS Big Sur)' do before do allow(RbConfig::CONFIG).to receive(:[]).and_return('darwin20') end it { should be_usable } end context 'on darwin10.0 (OS X Snow Leopard)' do before do allow(RbConfig::CONFIG).to receive(:[]).and_return('darwin10.0') end context 'with rb-fsevent > 0.9.4' do before { stub_const('FSEvent::VERSION', '0.9.6') } it 'shows a warning and should not be usable' do expect(Listen).to receive(:adapter_warn) expect(subject).to_not be_usable end end context 'with rb-fsevent <= 0.9.4' do before { stub_const('FSEvent::VERSION', '0.9.4') } it { should be_usable } end end context 'on another platform (linux)' do before { allow(RbConfig::CONFIG).to receive(:[]).and_return('linux') } 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 end listen-3.9.0/spec/lib/listen/adapter/linux_spec.rb000066400000000000000000000146201456645015000221460ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Listen::Adapter::Linux do describe 'class methods' do subject { described_class } if linux? it { should be_usable } else it { should_not be_usable } end end if linux? describe 'instance methods' do before(:all) do require 'rb-inotify' end let(:dir1) { Pathname.new("/foo/dir1") } let(:queue) { instance_double(Queue, "queue", close: nil) } let(:config) { instance_double(Listen::Adapter::Config, "config", queue: queue) } let(:silencer) { instance_double(Listen::Silencer, "silencer") } let(:snapshot) { instance_double(Listen::Change, "snapshot") } let(:record) { instance_double(Listen::Record, "record") } # TODO: fix other adapters too! subject { described_class.new(config) } after do subject.stop end describe 'watch events' do let(:directories) { [Pathname.pwd] } let(:adapter_options) { {} } let(:default_events) { [:recursive, :attrib, :create, :modify, :delete, :move, :close_write] } let(:fake_worker) { double(:fake_worker_for_watch_events) } let(:fake_notifier) { double(:fake_notifier, new: fake_worker) } before do 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(:silencer).and_return(silencer) allow(fake_worker).to receive(:close) end after do subject.stop end it 'starts by calling watch with default events' do expect(fake_worker).to receive(:watch).with(*directories.map(&:to_s), *default_events) subject.start end end describe 'inotify max watches exceeded' do let(:directories) { [Pathname.pwd] } let(:adapter_options) { {} } before do fake_worker = double(:fake_worker_for_inotify_limit_message) allow(fake_worker).to receive(:watch).and_raise(Errno::ENOSPC) allow(fake_worker).to receive(:close) fake_notifier = double(:fake_notifier, new: fake_worker) stub_const('INotify::Notifier', fake_notifier) allow(config).to receive(:directories).and_return(directories) allow(config).to receive(:adapter_options).and_return(adapter_options) end it 'raises exception' do expect { subject.start }.to raise_exception(Listen::Error::INotifyMaxWatchesExceeded, /inotify max watches exceeded/i) 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 fake_worker = double(:fake_worker_for_callback) events = [:recursive, :close_write] allow(fake_worker).to receive(:watch).with('/foo/dir1', *events) allow(fake_worker).to receive(:close) fake_notifier = double(:fake_notifier, new: fake_worker) stub_const('INotify::Notifier', fake_notifier) allow(config).to receive(:directories).and_return(directories) allow(config).to receive(:adapter_options).and_return(adapter_options) allow(config).to receive(:silencer).and_return(silencer) allow(Listen::Record).to receive(:new).with(dir1, silencer).and_return(record) allow(Listen::Change::Config).to receive(:new).with(queue, silencer). and_return(config) allow(Listen::Change).to receive(:new).with(config, record). and_return(snapshot) allow(subject).to receive(:require).with('rb-inotify') subject.configure end 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 describe '#stop' do let(:fake_worker) { double(:fake_worker_for_stop, close: true) } let(:directories) { [dir1] } let(:adapter_options) { { events: [:recursive, :close_write] } } before do allow(config).to receive(:directories).and_return(directories) allow(config).to receive(:adapter_options).and_return(adapter_options) end context 'when configured' do before do events = [:recursive, :close_write] allow(fake_worker).to receive(:watch).with('/foo/dir1', *events) fake_notifier = double(:fake_notifier, new: fake_worker) stub_const('INotify::Notifier', fake_notifier) allow(config).to receive(:silencer).and_return(silencer) allow(subject).to receive(:require).with('rb-inotify') subject.configure end it 'stops the worker' do subject.stop end end context 'when not even initialized' do before do allow(queue).to receive(:close) end it 'does not crash' do expect do subject.stop end.to_not raise_error end end end end end end listen-3.9.0/spec/lib/listen/adapter/polling_spec.rb000066400000000000000000000043541456645015000224560ustar00rootroot00000000000000# frozen_string_literal: true include 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, "config") } let(:directories) { [dir1] } let(:options) { {} } let(:queue) { instance_double(Queue, "queue") } let(:silencer) { instance_double(Listen::Silencer, "silencer") } let(:snapshot) { instance_double(Listen::Change, "snapshot") } let(:record) { instance_double(Listen::Record) } 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, silencer).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 after do allow(queue).to receive(:close) subject.stop 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 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.9.0/spec/lib/listen/adapter/windows_spec.rb000066400000000000000000000003541456645015000225000ustar00rootroot00000000000000# frozen_string_literal: true RSpec.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.9.0/spec/lib/listen/adapter_spec.rb000066400000000000000000000046051456645015000210110ustar00rootroot00000000000000# frozen_string_literal: true RSpec.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(Listen).to receive(:adapter_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(Listen).to receive(:adapter_warn).with(expected_msg) msg = 'custom fallback message' Listen::Adapter.select(polling_fallback_message: msg) end end end end listen-3.9.0/spec/lib/listen/backend_spec.rb000066400000000000000000000044001456645015000207510ustar00rootroot00000000000000# frozen_string_literal: true require '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 expect(adapter).to receive(:stop) subject.stop end end end listen-3.9.0/spec/lib/listen/change_spec.rb000066400000000000000000000063261456645015000206200ustar00rootroot00000000000000# frozen_string_literal: true RSpec.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.9.0/spec/lib/listen/cli_spec.rb000066400000000000000000000060001456645015000201270ustar00rootroot00000000000000# frozen_string_literal: true require '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(hash_including(relative: value)). and_return(listener) described_class.new(relative: value).start end end listen-3.9.0/spec/lib/listen/directory_spec.rb000066400000000000000000000207661456645015000214030ustar00rootroot00000000000000# frozen_string_literal: true include 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 def fake_children(exception, dir, *args, &block) if block_given? exception.send(:allow, dir).to receive(:children, &block) else exception.send(:allow, dir).to receive(:children).and_return(*args) end exception.send(:allow, dir).to receive(:exist?).and_return(true) exception.send(:allow, dir).to receive(:directory?).and_return(true) 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 { fake_children(self, dir, []) } 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 fake_children(self, dir, [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 fake_children(self, dir, [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 file.rb no longer exists after scan' do before do fake_children(self, dir, [file], [file2]) allow(::File).to receive(:lstat).with('file.rb'). and_raise(Errno::ENOENT) allow(::File).to receive(:lstat).with('file2.rb'). and_return(fake_file_stat('file2.rb')) end it 'rescans' do expect(snapshot).to receive(:invalidate).with(:file, 'file2.rb', {}) described_class.scan(snapshot, '.', options) end end context 'when file2.rb is added' do before do fake_children(self, dir, [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 { fake_children(self, dir) { 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 { fake_children(self, dir) { 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 fake_children(self, dir, [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 { fake_children(self, dir, []) } 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 fake_children(self, dir, [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 fake_children(self, dir) { 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 fake_children(self, dir, [subdir]) fake_children(self, subdir, []) 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.9.0/spec/lib/listen/event/000077500000000000000000000000001456645015000171465ustar00rootroot00000000000000listen-3.9.0/spec/lib/listen/event/config_spec.rb000066400000000000000000000014341456645015000217540ustar00rootroot00000000000000# frozen_string_literal: true require '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.9.0/spec/lib/listen/event/loop_spec.rb000066400000000000000000000060511456645015000214600ustar00rootroot00000000000000# frozen_string_literal: true require 'thread' require 'listen/event/config' require 'listen/event/loop' 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, 'thread') } let(:reasons) { instance_double(::Queue, 'reasons') } let(:ready) { instance_double(::Queue, 'ready') } let(:blocks) do { thread_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(Thread).to receive(:new) do |*args, &block| fail 'Unstubbed call:'\ " Thread.new(#{args.map(&:inspect) * ','},&#{block.inspect})" end allow(config).to receive(:min_delay_between_events).and_return(1.234) allow(thread).to receive(:name=) allow(Thread).to receive(:new) do |*_, &block| blocks[:thread_block] = block thread end allow(Kernel).to receive(:sleep) do |*args| fail "stub called: sleep(#{args.map(&:inspect) * ','})" end end describe '#start' do it 'is started' do expect(processor).to receive(:loop_for).with(1.234) expect(Thread).to receive(:new) do |&block| block.call thread end subject.start expect(subject).to be_started end context 'when start is called again' do it 'returns silently' do expect(processor).to receive(:loop_for).with(1.234) expect(Thread).to receive(:new) do |&block| block.call thread end subject.start expect { subject.start }.to_not raise_exception end end context 'when state change to :started takes longer than 5 seconds' do before do expect(Thread).to receive(:new) { thread } expect_any_instance_of(::ConditionVariable).to receive(:wait) { } # return immediately end it 'raises Error::NotStarted' do expect do subject.start end.to raise_exception(::Listen::Error::NotStarted, "thread didn't start in 5.0 seconds (in state: :starting)") end end end context 'when set up / started' do before do allow(thread).to receive(:alive?).and_return(true) allow(config).to receive(:min_delay_between_events).and_return(1.234) allow(processor).to receive(:loop_for).with(1.234) expect(Thread).to receive(:new) do |&block| block.call thread end subject.start end describe '#stop' do before do allow(thread).to receive(:join) end it 'frees the thread' do subject.stop end it 'waits for the thread to finish' do expect(thread).to receive(:join) subject.stop end it 'sets the reason for waking up' do subject.stop end end end end listen-3.9.0/spec/lib/listen/event/processor_spec.rb000066400000000000000000000207051456645015000225300ustar00rootroot00000000000000# frozen_string_literal: true require 'listen/event/processor' require 'listen/event/config' RSpec.describe Listen::Event::Processor do let(:event_queue) { instance_double(::Queue, 'event_queue') } let(:event) { instance_double(::Array, 'event') } let(:listener) { instance_double(Listen::Listener, 'listener') } let(:config) { instance_double(Listen::Event::Config, 'config', listener: listener) } 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 { monotonic_time: 0.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(listener).to receive(:stopped?) do status_for_time(state[:monotonic_time]) == :stopped end allow(listener).to receive(:paused?) do status_for_time(state[:monotonic_time]) == :paused end allow(Listen::MonotonicTime).to receive(:now) do state[:monotonic_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 expect(event_queue).to receive(:pop).and_return(event) subject.loop_for(1) end it 'does not sleep' do expect(config).to_not receive(:sleep) expect(event_queue).to receive(:pop).and_return(event) t = Listen::MonotonicTime.now subject.loop_for(1) diff = Listen::MonotonicTime.now - t expect(diff).to be < 0.02 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[:monotonic_time] = 0.6 } expect(event_queue).to receive(:pop).and_return(event) subject.loop_for(1) end it 'breaks' do expect(event_queue).to receive(:pop).and_return(event) allow(config).to receive(:sleep).once { state[:monotonic_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(anything) { |*_args| state[:monotonic_time] += 0.6 } # pretend we slept for latency (now: 1.6 seconds since start) allow(config).to receive(:sleep). with(1.0) { |*_args| state[:monotonic_time] += 1.0 } expect(event_queue).to receive(:pop).and_return(event) 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(anything) { |*_args| state[:monotonic_time] += 2.0 } # 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(listener).to receive(:wait_for_state). with(:initializing, :backend_started, :processing_events, :stopped) do |*_args| state[:monotonic_time] += 3.0 raise ScriptError, 'done' end expect(event_queue).to receive(:pop).and_return(event) expect { subject.loop_for(1) }.to raise_exception(ScriptError, 'done') 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(event_queue).to receive(:pop).and_return(event) expect(config).to receive(:sleep). once { |*_args| state[:monotonic_time] = 0.6 } subject.loop_for(1) end it 'breaks' do allow(config).to receive(:sleep). once { |*_args| state[:monotonic_time] = 0.6 } expect(config).to_not receive(:call) expect(event_queue).to receive(:pop).and_return(event) subject.loop_for(1) end end end context 'when event queue has events' do 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).exactly(4) allow(config).to receive(:optimize_changes).with([change, change, change]). and_return(resulting_changes) final_changes = [['foo'], [], []] allow(config).to receive(:call) do |*changes| state[:monotonic_time] = 4.0 # stopped expect(changes).to eq(final_changes) end allow(listener).to receive(:wait_for_state). with(:initializing, :backend_started, :processing_events, :stopped) subject.instance_variable_set(:@_remember_time_of_first_unprocessed_event, -3) subject.loop_for(1) end end end end end end describe '_process_changes' do context 'when it raises an exception derived from StandardError' do before do allow(event_queue).to receive(:empty?).and_return(true) allow(config).to receive(:callable?).and_return(true) resulting_changes = { modified: ['foo'], added: [], removed: [] } allow(config).to receive(:optimize_changes).with(anything).and_return(resulting_changes) expect(config).to receive(:call).and_raise(ArgumentError, "bang!") expect(config).to receive(:call).and_return(nil) expect(config).to receive(:call).and_raise("error!") end it 'rescues and logs exception and continues' do expect(Listen.logger).to receive(:error).with(/Exception rescued in _process_changes:\nArgumentError: bang!/) expect(Listen.logger).to receive(:error).with(/Exception rescued in _process_changes:\nRuntimeError: error!/) expect(Listen.logger).to receive(:debug).with(/Callback \(exception\) took/) expect(Listen.logger).to receive(:debug).with(/Callback took/) expect(Listen.logger).to receive(:debug).with(/Callback \(exception\) took/) subject.send(:_process_changes, event) subject.send(:_process_changes, event) subject.send(:_process_changes, event) end end end end listen-3.9.0/spec/lib/listen/event/queue_spec.rb000066400000000000000000000063561456645015000216430ustar00rootroot00000000000000# frozen_string_literal: true require '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 } subject { described_class.new(config) } 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 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.9.0/spec/lib/listen/file_spec.rb000066400000000000000000000171421456645015000203100ustar00rootroot00000000000000# frozen_string_literal: true RSpec.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), size: kind_of(Integer) } end context 'with file record' do let(:record_mtime) { nil } let(:record_sha) { nil } let(:record_mode) { nil } let(:record_size) { nil } let(:record_data) do { mtime: record_mtime, sha: record_sha, mode: record_mode, size: record_size } 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(:record_size) { 42 } let(:stat_size) { record_size } let(:sha) { fail 'stub me (sha)' } let(:stat) do instance_double( File::Stat, mtime: stat_mtime, atime: stat_atime, ctime: stat_ctime, mode: stat_mode, size: stat_size ) end before do allow(::File).to receive(:lstat) { stat } allow(Digest::SHA256).to receive(:file) { double(:sha, digest: sha) } 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 identical 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).utc } let(:stat_atime) { Time.at(1_401_235_714.123).utc } let(:stat_ctime) { Time.at(1_401_235_714.123).utc } 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).utc } let(:stat_atime) { Time.at(1_401_235_714.0).utc } let(:stat_ctime) { Time.at(1_401_235_714.0).utc } 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).utc } 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).utc } before { allow(Time).to receive(:now) { now } } context 'without available sha' do let(:sha) { 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 sha' do let(:sha) { 'd41d8cd98f00b204e9800998ecf8427e' } context 'with same sha in record' do let(:record_sha) { sha } it { should be_nil } end context 'with no sha in record' do let(:record_sha) { nil } it { should be_nil } end context 'with different sha in record' do let(:record_sha) { 'foo' } it { should be :modified } it 'sets path in record with expected data' do expected = expected_data.merge(sha: sha) 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, size: 0 ) 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, size: 0) end subject { Listen::File.inaccurate_mac_time?(stat) } context 'with no accurate times' do let(:mtime) { Time.at(1_234_567.0).utc } let(:atime) { Time.at(1_234_567.0).utc } let(:ctime) { Time.at(1_234_567.0).utc } it { should be_truthy } end context 'with all accurate times' do let(:mtime) { Time.at(1_234_567.89).utc } let(:atime) { Time.at(1_234_567.89).utc } let(:ctime) { Time.at(1_234_567.89).utc } it { should be_falsey } end context 'with one accurate time' do let(:mtime) { Time.at(1_234_567.0).utc } let(:atime) { Time.at(1_234_567.89).utc } let(:ctime) { Time.at(1_234_567.0).utc } it { should be_falsey } end end end listen-3.9.0/spec/lib/listen/fsm_spec.rb000066400000000000000000000100361456645015000201510ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Listen::FSM do context 'simple FSM' do class SpecSimpleFsm include Listen::FSM attr_reader :entered_started start_state :initial state :started, to: :stopped do @entered_started = true end state :failed, to: :stopped state :stopped def start transition(:started) end def stop transition(:stopped) end def fail transition(:failed) end def initialize initialize_fsm end end subject(:fsm) { SpecSimpleFsm.new } it 'starts in start_state' do expect(subject.state).to eq(:initial) end it 'allows transitions' do subject.start expect(subject.state).to eq(:started) expect(subject.entered_started).to eq(true) end it 'raises on disallowed transitions' do subject.fail expect do subject.start end.to raise_exception(ArgumentError, "SpecSimpleFsm can't change state from 'failed' to 'started', only to: stopped") expect(subject.state).to eq(:failed) expect(subject.entered_started).to eq(nil) end it 'declares transition and transition! private' do expect { subject.transition(:started) }.to raise_exception(NoMethodError, /private.*transition/) expect { subject.transition!(:started) }.to raise_exception(NoMethodError, /private.*transition!/) end describe '#wait_for_state' do it 'returns truthy immediately if already in the desired state' do expect(subject.instance_variable_get(:@state_changed)).to_not receive(:wait) result = subject.wait_for_state(:initial) expect(result).to be_truthy end it 'waits for the next state change and returns truthy if then in the desired state' do expect(subject.instance_variable_get(:@state_changed)).to receive(:wait).with(anything, anything) do subject.instance_variable_set(:@state, :started) end result = subject.wait_for_state(:started) expect(result).to be_truthy end it 'waits for the next state change and returns falsey if then not the desired state' do expect(subject.instance_variable_get(:@state_changed)).to receive(:wait).with(anything, anything) result = subject.wait_for_state(:started) expect(result).to be_falsey end it 'passes the timeout: down to wait, if given' do expect(subject.instance_variable_get(:@state_changed)).to receive(:wait).with(anything, 5.0) subject.wait_for_state(:started, timeout: 5.0) end it 'passes nil (infinite) timeout: down to wait, if none given' do expect(subject.instance_variable_get(:@state_changed)).to receive(:wait).with(anything, nil) subject.wait_for_state(:started) end it 'enforces precondition that states must be symbols' do expect do subject.wait_for_state(:started, 'stopped') end.to raise_exception(ArgumentError, /states must be symbols .*got "stopped"/) end end end context 'FSM with no start state' do class SpecFsmWithNoStartState include Listen::FSM state :started, to: :stopped state :failed, to: :stopped state :stopped def initialize initialize_fsm end end subject(:fsm) { SpecFsmWithNoStartState.new } it 'raises ArgumentError on new' do expect { subject }.to raise_exception(ArgumentError, /`start_state :` must be declared before `new`/) end end context 'FSM with string state name' do subject(:fsm) do instance_exec do class SpecFsmWithStringState include Listen::FSM state 'started', to: 'stopped' state 'stopped' def initialize initialize_fsm end end end end it 'raises ArgumentError on new' do expect { subject }.to raise_exception(ArgumentError, /state name must be a Symbol/) end end end listen-3.9.0/spec/lib/listen/listener/000077500000000000000000000000001456645015000176525ustar00rootroot00000000000000listen-3.9.0/spec/lib/listen/listener/config_spec.rb000066400000000000000000000014351456645015000224610ustar00rootroot00000000000000# frozen_string_literal: true require '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 }.freeze 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.9.0/spec/lib/listen/listener_spec.rb000066400000000000000000000200721456645015000212120ustar00rootroot00000000000000# frozen_string_literal: true include 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(Thread).to receive(:new).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 do described_class.new('dir1') do |*args| myblock.call(*args) end end 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(%w[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(:start) subject.start expect(subject).to_not be_paused end it 'starts adapter' do expect(backend).to receive(:start) allow(processor).to receive(:start) subject.start end end describe '#stop' do before do allow(backend).to receive(:start) allow(processor).to receive(:start) end context 'when fully started' do before do subject.start end it 'terminates' do allow(backend).to receive(:stop) allow(processor).to receive(:stop) subject.stop end end context 'when only initialized' do before do subject end it 'terminates' do allow(backend).to receive(:stop) allow(processor).to receive(:stop) subject.stop end end end describe '#pause' do before do allow(backend).to receive(:start) allow(processor).to receive(:start) 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(:start) 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(:start) 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(:start) 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(:start) 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 end listen-3.9.0/spec/lib/listen/logger_spec.rb000066400000000000000000000134301456645015000206440ustar00rootroot00000000000000# frozen_string_literal: true require 'listen/logger' RSpec.describe 'logger.rb' do around do |spec| orig_logger = Listen.instance_variable_get(:@logger) spec.run Listen.logger = orig_logger end describe 'Listen.logger' do ENV_VARIABLE_NAME = 'LISTEN_GEM_DEBUGGING' let(:logger) { instance_double(::Logger, "logger") } around do |spec| orig_debugging_env_variable = ENV.fetch(ENV_VARIABLE_NAME, :not_set) spec.run if orig_debugging_env_variable == :not_set ENV.delete(ENV_VARIABLE_NAME) else ENV[ENV_VARIABLE_NAME] = orig_debugging_env_variable end end describe 'logger=' do it 'allows the logger to be set' do Listen.logger = logger expect(Listen.logger).to be(logger) end it 'allows nil to be set (implying default logger)' do Listen.logger = nil expect(Listen.logger).to be_kind_of(::Logger) end end describe 'logger' do before do Listen.instance_variable_set(:@logger, nil) end it 'returns default logger if none set' do expect(Listen.logger).to be_kind_of(::Logger) end ['debug', 'DEBUG', '2', 'level2', '2 '].each do |env_value| it "infers DEBUG level from #{ENV_VARIABLE_NAME}=#{env_value.inspect}" do ENV[ENV_VARIABLE_NAME] = env_value expect(Listen.logger.level).to eq(::Logger::DEBUG) end end ['info', 'INFO', 'true', ' true', 'TRUE', 'TRUE ', 'yes', 'YES', ' yesss!', '1', 'level1'].each do |env_value| it "infers INFO level from #{ENV_VARIABLE_NAME}=#{env_value.inspect}" do ENV[ENV_VARIABLE_NAME] = env_value expect(Listen.logger.level).to eq(::Logger::INFO) end end ['warn', 'WARN', ' warn', 'warning'].each do |env_value| it "infers WARN level from #{ENV_VARIABLE_NAME}=#{env_value.inspect}" do ENV[ENV_VARIABLE_NAME] = env_value expect(Listen.logger.level).to eq(::Logger::WARN) end end ['error', 'ERROR', 'OTHER'].each do |env_value| it "infers ERROR level from #{ENV_VARIABLE_NAME}=#{env_value.inspect}" do ENV[ENV_VARIABLE_NAME] = env_value expect(Listen.logger.level).to eq(::Logger::ERROR) end end ['fatal', 'FATAL', ' fatal'].each do |env_value| it "infers FATAL level from #{ENV_VARIABLE_NAME}=#{env_value.inspect}" do ENV[ENV_VARIABLE_NAME] = env_value expect(Listen.logger.level).to eq(::Logger::FATAL) end end end end describe 'Listen.adapter_warn_behavior' do subject { Listen.adapter_warn(message) } after do Listen.adapter_warn_behavior = :warn end let(:message) { "warning message" } it 'defaults to :warn' do expect(Listen.adapter_warn_behavior).to eq(:warn) expect(Listen).to receive(:warn).with(message) subject end it 'allows the adapter_warn_behavior to be set to :log' do Listen.adapter_warn_behavior = :log expect(Listen.logger).to receive(:warn).with(message) subject end [:silent, nil, false].each do |behavior| it "allows the adapter_warn_behavior to be set to #{behavior} to silence the warnings" do Listen.adapter_warn_behavior = behavior expect(Listen.logger).not_to receive(:warn) expect(Listen).not_to receive(:warn) subject end end context "when LISTEN_GEM_ADAPTER_WARN_BEHAVIOR is set to 'log'" do around do |spec| orig_debugging_env_variable = ENV.fetch('LISTEN_GEM_ADAPTER_WARN_BEHAVIOR', :not_set) ENV['LISTEN_GEM_ADAPTER_WARN_BEHAVIOR'] = 'log' spec.run if orig_debugging_env_variable == :not_set ENV.delete('LISTEN_GEM_ADAPTER_WARN_BEHAVIOR') else ENV['ENV_VARIABLE_NAME'] = orig_debugging_env_variable end end [:silent, nil, false, :warn].each do |behavior| it "respects the environment variable over #{behavior.inspect}" do Listen.adapter_warn_behavior = behavior expect(Listen.logger).to receive(:warn).with(message) subject end end it "respects the environment variable over a callable config" do Listen.adapter_warn_behavior = ->(_message) { :warn } expect(Listen.logger).to receive(:warn).with(message) subject end end context 'when adapter_warn_behavior is set to a callable object like a proc' do before do Listen.adapter_warn_behavior = ->(message) do case message when /USE warn/ :warn when /USE log/ :log when /USE silent/ :silent when /USE false/ false when /USE nil/ nil else true end end end [true, :warn].each do |behavior| context "when the message matches a #{behavior.inspect} pattern" do let(:message) { "USE #{behavior.inspect}" } it 'respects :warn' do expect(Listen).to receive(:warn).with(message) subject end end end context 'when the message matches a :silent pattern' do let(:message) { "USE silent" } it 'respects :silent' do expect(Listen).not_to receive(:warn).with(message) expect(Listen).not_to receive(:warn) subject end end [false, nil].each do |behavior| context 'when the message matches a #{behavior} pattern' do let(:message) { "USE #{behavior.inspect}" } it 'respects :silent' do expect(Listen).not_to receive(:warn).with(message) expect(Listen).not_to receive(:warn) subject end end end end end end listen-3.9.0/spec/lib/listen/monotonic_time_spec.rb000066400000000000000000000034431456645015000224130ustar00rootroot00000000000000# frozen_string_literal: true require 'listen/monotonic_time' RSpec.describe Listen::MonotonicTime do after(:all) do # load once more with constants unstubbed/unhidden load './lib/listen/monotonic_time.rb' end context 'module methods' do describe '.now' do subject { described_class.now } let(:tick_count) { 0.123 } context 'when CLOCK_MONOTONIC defined' do before do stub_const('Process::CLOCK_MONOTONIC', 10) load './lib/listen/monotonic_time.rb' end it 'returns the CLOCK_MONOTONIC tick count' do expect(Process).to receive(:clock_gettime).with(Process::CLOCK_MONOTONIC).and_return(tick_count) expect(subject).to eq(tick_count) end end context 'when CLOCK_MONOTONIC not defined but CLOCK_MONOTONIC_RAW defined' do before do hide_const('Process::CLOCK_MONOTONIC') stub_const('Process::CLOCK_MONOTONIC_RAW', 11) load './lib/listen/monotonic_time.rb' end it 'returns the floating point Time.now' do expect(Process).to receive(:clock_gettime).with(Process::CLOCK_MONOTONIC_RAW).and_return(tick_count) expect(subject).to eq(tick_count) end end context 'when neither CLOCK_MONOTONIC nor CLOCK_MONOTONIC_RAW defined' do let(:now) { instance_double(Time, "time") } before do hide_const('Process::CLOCK_MONOTONIC') hide_const('Process::CLOCK_MONOTONIC_RAW') load './lib/listen/monotonic_time.rb' end it 'returns the floating point Time.now' do expect(Time).to receive(:now).and_return(now) expect(now).to receive(:to_f).and_return(tick_count) expect(subject).to eq(tick_count) end end end end end listen-3.9.0/spec/lib/listen/queue_optimizer_spec.rb000066400000000000000000000064641456645015000226240ustar00rootroot00000000000000# frozen_string_literal: true RSpec.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 detected 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.9.0/spec/lib/listen/record_spec.rb000066400000000000000000000274561456645015000206600ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Listen::Record do let(:dir) { instance_double(Pathname, to_s: '/dir') } let(:silencer_options) { { ignore!: [/\A\.ignored/] } } let(:silencer) { Listen::Silencer.new(**silencer_options) } let(:record) { Listen::Record.new(dir, silencer) } 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, size: 42) 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 hash_or_dir.is_a?(String) 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 splitting 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 'when there is a file with the same name as a dir' do subject { record.dir_entries('cypress') } before do record.update_file('cypress.json', mtime: 1.1) record.update_file('cypress/README.md', mtime: 1.2) record.update_file('a/b/cypress/d', mtime: 1.3) record.update_file('a/b/c/cypress', mtime: 1.3) end it { should eq('README.md' => { mtime: 1.2 }) } end context 'when there is a file with a similar name to a dir' do subject { record.dir_entries('app') } before do record.update_file('appspec.yml', mtime: 1.1) record.update_file('app/README.md', mtime: 1.2) record.update_file('spec/app/foo', mtime: 1.3) end it { should eq('README.md' => { mtime: 1.2 }) } 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 context 'with empty path/subdir' do before { record.add_dir('path/subdir') } it { should be_empty } end context 'with path/subdir with file' do before do record.add_dir('path/subdir') record.update_file('path/subdir/file.rb', mtime: 1.1) end it { should be_empty } end context 'with path renamed to file' do before do record.add_dir('path/subdir') record.update_file('path', mtime: 1.1) end it { should be_empty } 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, size: 42) } let(:bar_stat) { instance_double(::File::Stat, mtime: 2.3, mode: 0755, size: 42) } 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, size: 42 }, 'bar' => { mtime: 2.3, mode: 0755, size: 42 }) end end context 'with subdir containing files' do before do real_directory('/dir' => %w[dir1 dir2 .ignored]) real_directory('/dir/dir1' => %w[foo]) real_directory('/dir/dir1/foo' => %w[bar]) lstat(file('/dir/.ignored/FETCH_HEAD')) lstat(file('/dir/dir1/foo/bar')) real_directory('/dir/dir2' => []) end it 'builds record, skipping silenced patterns' do record.build expect(record_tree(record)). to eq( 'dir1' => {}, 'dir1/foo' => { 'bar' => { mtime: 2.3, mode: 0755, size: 42 } }, 'dir2' => {} ) end end context 'with subdir containing dirs' do before do real_directory('/dir' => %w[dir1 dir2 .ignored]) real_directory('/dir/.ignored' => %w[ignored_file]) 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_any_instance_of(Listen::Record::SymlinkDetector).to receive(:warn). 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.9.0/spec/lib/listen/silencer/000077500000000000000000000000001456645015000176315ustar00rootroot00000000000000listen-3.9.0/spec/lib/listen/silencer/controller_spec.rb000066400000000000000000000057051456645015000233620ustar00rootroot00000000000000# frozen_string_literal: true require '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.9.0/spec/lib/listen/silencer_spec.rb000066400000000000000000000060531456645015000211740ustar00rootroot00000000000000# frozen_string_literal: true RSpec::Matchers.define :accept do |type, path| match { |actual| !actual.silenced?(Pathname(path), type) } end RSpec.describe Listen::Silencer do let(:options) { {} } subject { described_class.new(**options) } describe '#silenced?' do it { should accept(:file, Pathname('some_dir').join("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] # Emacs backup/swap files ignored += %w[#hello.rb# .#hello.rb] # sed temp files ignored += %w[sedq7eVAR sed86w1kB] # mutagen temp files ignored += %w[ .mutagen-temporary-cross-device-rename0 .mutagen-temporary-unicode-test-éntry0 ] 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: [%r{^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: [%r{^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.9.0/spec/lib/listen/thread_spec.rb000066400000000000000000000071471456645015000206440ustar00rootroot00000000000000# frozen_string_literal: true require 'listen/thread' RSpec.describe Listen::Thread do let(:raise_nested_exception_block) do -> do begin begin raise ArgumentError, 'boom!' rescue raise 'nested inner' end rescue raise 'nested outer' end end end let(:raise_script_error_block) do -> do raise ScriptError, "ruby typo!" end end describe '.new' do let(:name) { "worker_thread" } let(:block) { -> { } } subject { described_class.new(name, &block) } it "calls Thread.new" do expect(Thread).to receive(:new) do thread = instance_double(Thread, "thread") expect(thread).to receive(:name=).with("listen-#{name}") thread end subject end context "when exception raised" do let(:block) do -> { raise ArgumentError, 'boom!' } end it "rescues and logs exceptions" do pattern = <<~EOS.strip Exception rescued in listen-worker_thread: ArgumentError: boom! .*\\/listen\\/thread_spec\\.rb EOS expect(Listen.logger).to receive(:error).with(/#{pattern}/) subject.join end it "rescues and logs backtrace + exception backtrace" do pattern = <<~EOS.strip Exception rescued in listen-worker_thread: ArgumentError: boom! .*\\/listen\\/thread\\.rb.*--- Thread.new ---.*\\/listen\\/thread_spec\\.rb EOS expect(Listen.logger).to receive(:error).with(/#{pattern}/m) subject.join end end class TestExceptionDerivedFromException < Exception; end # rubocop:disable Lint/InheritException context "when exception raised that is not derived from StandardError" do [SystemExit, SystemStackError, NoMemoryError, SecurityError, TestExceptionDerivedFromException].each do |exception| context exception.name do let(:block) do -> { raise exception, 'boom!' } end it "does not rescue" do expect(Thread).to receive(:new) do |&block| expect do block.call end.to raise_exception(exception, 'boom!') thread = instance_double(Thread, "thread") allow(thread).to receive(:name=).with(any_args) thread end subject end end end end context "when nested exceptions raised" do let(:block) { raise_nested_exception_block } it "details exception causes" do pattern = <<~EOS RuntimeError: nested outer --- Caused by: --- RuntimeError: nested inner --- Caused by: --- ArgumentError: boom! EOS expect(Listen.logger).to receive(:error).with(/#{pattern}/) subject.join end end end describe '.rescue_and_log' do it 'rescues and logs nested exceptions' do pattern = <<~EOS Exception rescued in method: RuntimeError: nested outer --- Caused by: --- RuntimeError: nested inner --- Caused by: --- ArgumentError: boom! EOS expect(Listen.logger).to receive(:error).with(/#{pattern}/) described_class.rescue_and_log("method", &raise_nested_exception_block) end context 'when exception raised that is not derived from StandardError' do let(:block) { raise_script_error_block } it "raises out" do expect do described_class.rescue_and_log("method", &raise_script_error_block) end.to raise_exception(ScriptError, "ruby typo!") end end end end listen-3.9.0/spec/lib/listen_spec.rb000066400000000000000000000010741456645015000173660ustar00rootroot00000000000000# frozen_string_literal: true RSpec.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.9.0/spec/spec_helper.rb000066400000000000000000000021351456645015000166000ustar00rootroot00000000000000# frozen_string_literal: true # TODO: reduce requires everwhere and be more strict about it require 'listen' Listen.logger.level = Logger::WARN unless ENV['LISTEN_GEM_DEBUGGING'] def ci? ENV['CI'] end if ci? require 'coveralls' Coveralls.wear! end Dir["#{__dir__}/support/**/*.rb"].sort.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 RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length = 2_000 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, directory?: true }.merge(options)) end end RSpec.configure do |config| config.include SpecHelpers end Thread.abort_on_exception = true listen-3.9.0/spec/support/000077500000000000000000000000001456645015000154755ustar00rootroot00000000000000listen-3.9.0/spec/support/acceptance_helper.rb000066400000000000000000000200371456645015000214510ustar00rootroot00000000000000# frozen_string_literal: true { 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 reset_queue = /queued_/ !~ description actual.listen(reset_queue: reset_queue) do change_fs(type, expected) if reset_queue end actual.changes[type].include? expected end failure_message do |actual| "expected #{actual.changes.inspect} to include #{description} of #{expected}" end failure_message_when_negated do |actual| "expected #{actual.changes.inspect} to not include #{description} of #{expected}" end end end # rubocop:disable Metrics/MethodLength def change_fs(type, path) case type when :modified File.exist?(path) or fail "Bad test: cannot modify #{path.inspect} (it doesn't exist)" # 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) File.open(path, 'a') { |f| f.write('foo') } # separate it from upcoming modifications" _sleep_to_separate_events when :added File.exist?(path) and fail "Bad test: cannot add #{path.inspect} (it already exists)" # 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) File.write(path, 'foo') # separate it from upcoming modifications" _sleep_to_separate_events when :removed File.exist?(path) or fail "Bad test: cannot remove #{path.inspect} (it doesn't exist)" File.unlink(path) else fail "bad test: unknown type: #{type.inspect}" end end # rubocop:enable Metrics/MethodLength # 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 (sha), 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.utc 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 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'] || 1.0) @paths = paths # Isolate collected changes between tests/listener instances @timed_changes = TimedChanges.new @listener = if callback 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) callback.call(modified, added, removed) unless callback == :track_changes end else 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: 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 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 raise "Changes took #{change_offset}s (allowed lag: #{freeze_offset})s" end def _relative_path(changes) changes.map do |change| unfrozen_copy = change.dup [@paths].flatten.each do |path| sub = path.sub(%r{/$}, '').to_s unfrozen_copy.gsub!(%r{^#{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 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.9.0/spec/support/fixtures_helper.rb000066400000000000000000000015021456645015000212300ustar00rootroot00000000000000# frozen_string_literal: true require '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.9.0/spec/support/platform_helper.rb000066400000000000000000000004451456645015000212100ustar00rootroot00000000000000# frozen_string_literal: true def 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