listen-compat 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.rubocop.yml +2 -0
- data/.rubocop_todo.yml +14 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +139 -0
- data/Rakefile +10 -0
- data/example.rb +12 -0
- data/lib/listen/compat.rb +2 -0
- data/lib/listen/compat/test.rb +1 -0
- data/lib/listen/compat/test/fake.rb +61 -0
- data/lib/listen/compat/test/session.rb +70 -0
- data/lib/listen/compat/test/simple.rb +15 -0
- data/lib/listen/compat/version.rb +5 -0
- data/lib/listen/compat/wrapper.rb +180 -0
- data/listen-compat.gemspec +25 -0
- data/spec/listen/compat/example_spec.rb +39 -0
- data/spec/listen/compat/wrapper_spec.rb +228 -0
- data/spec/spec_helper.rb +73 -0
- metadata +97 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: bac4b0ee225554a721aaa8daada780e569fbdc8c
|
4
|
+
data.tar.gz: 035bc356f079cea0a7ded926c471ac93a7cc8435
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b8b7b7f57c0011f7e6cbb19c902fb1ad1a15e801d4ceca0a3f0df519c1ec89122991f9855cc81e8314fdc1a6d83dae28d205ff1208fc2250fea58a1f1656aa0e
|
7
|
+
data.tar.gz: 7b8c356db968632ae02e95c6348e0cfc67efd9de70d3644c362b51f9d8cfc9f2ed3f5e41d6341ae811c1a4b28a6d1d5aa18ae1676f57cc26ce38ed6c34f491f8
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/.rubocop_todo.yml
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# This configuration was generated by `rubocop --auto-gen-config`
|
2
|
+
# on 2014-11-27 16:11:17 +0100 using RuboCop version 0.27.1.
|
3
|
+
# The point is for the user to remove these configuration records
|
4
|
+
# one by one as the offenses are removed from the code base.
|
5
|
+
# Note that changes in the inspected code, or installation of new
|
6
|
+
# versions of RuboCop, may require this file to be generated again.
|
7
|
+
|
8
|
+
# Offense count: 1
|
9
|
+
Lint/HandleExceptions:
|
10
|
+
Enabled: false
|
11
|
+
|
12
|
+
# Offense count: 6
|
13
|
+
Style/Documentation:
|
14
|
+
Enabled: false
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Cezary Baginski
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
# Listen::Compat
|
2
|
+
|
3
|
+
A wrapper for [Listen](https://github.com/guard/listen) to "guarantee" a
|
4
|
+
simplified and unchanging (future-compatible) API for cross-platform watching
|
5
|
+
files and directories.
|
6
|
+
|
7
|
+
It is designed to work with any version of Listen installed (it contains
|
8
|
+
workarounds for buggy or incomplete old versions).
|
9
|
+
|
10
|
+
This is useful for app/gem developers who want to "just watch files until
|
11
|
+
Ctrl-C". (And not care about historical incompatibilities or bug fixes /
|
12
|
+
regressions related to Listen).
|
13
|
+
|
14
|
+
It also helps easily write unit tests for using listen in other apps, without
|
15
|
+
having to deal with threads, locks, queues, sleeping, Listen API changes, etc.
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
As long as users of you application have *any* version of Listen installed,
|
20
|
+
listen-compat should work.
|
21
|
+
|
22
|
+
Example Gemfile:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
gem 'listen-compat'
|
26
|
+
gem 'listen' # hopefully, any version you like will work
|
27
|
+
```
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
You can assume the following interface will never change.
|
32
|
+
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
require 'listen/compat/wrapper'
|
36
|
+
|
37
|
+
listener = Listen::Compat::Wrapper.create
|
38
|
+
|
39
|
+
directories = %w(foo bar baz)
|
40
|
+
options = { force_polling: false }
|
41
|
+
|
42
|
+
listener.listen(directories, options) do |modified, added, removed|
|
43
|
+
puts "Modified: #{modified.inspect}"
|
44
|
+
puts "Added: #{added.inspect}"
|
45
|
+
puts "Removed: #{removed.inspect}"
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
(You can assume compatibility will become better and more robust without having
|
50
|
+
to make any changes in your app's code.)
|
51
|
+
|
52
|
+
|
53
|
+
## Details
|
54
|
+
|
55
|
+
This will always be guaranteed to include everything necessary:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
require 'listen/compat/wrapper'
|
59
|
+
```
|
60
|
+
|
61
|
+
The following will do whatever magic necessary to find a usable version of
|
62
|
+
Listen, require it, and return the right wrapper.
|
63
|
+
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
listener = Listen::Compat::Wrapper.create
|
67
|
+
```
|
68
|
+
|
69
|
+
You can assume directories can be passed in any form (relative, absolute,
|
70
|
+
Pathname, real-only directories) and any encoding and this gem's
|
71
|
+
responsibility is to deal with it.
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
directories = %w(foo bar baz)
|
75
|
+
```
|
76
|
+
|
77
|
+
While different version of listen support different options, it's
|
78
|
+
Listen-Compat's responsibility to make sure they are properly translated or
|
79
|
+
ignored, and possible to set without changes in your code (e.g. environment
|
80
|
+
variables, listen config files, etc.)
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
options = { force_polling: false }
|
84
|
+
```
|
85
|
+
|
86
|
+
You can assume your callback will always receive an array of 3 arrays:
|
87
|
+
- possibly changed files (but maybe no longer existing)
|
88
|
+
- possibly added files (but maybe no longer existing)
|
89
|
+
- possibly removed files (but may be existing again)
|
90
|
+
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
listener.listen(directories, options) do |modified, added, removed|
|
94
|
+
```
|
95
|
+
|
96
|
+
## Unit testing Listen in you app
|
97
|
+
|
98
|
+
Listen-compat provides a fast-enough and thorough enough test helper for you to
|
99
|
+
just add a single unit test to accurately simulate a real blocking session with
|
100
|
+
Listen.
|
101
|
+
|
102
|
+
It also lets you conveniently use relative files for simulating events (even
|
103
|
+
though the actual Listen implementation reports full paths).
|
104
|
+
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
require "listen/compat/test/session"
|
108
|
+
|
109
|
+
def test_if_watching_files_works
|
110
|
+
|
111
|
+
session = Listen::Compat::Test::Session.new do
|
112
|
+
# put whatever code would cause Listen to block here:
|
113
|
+
myapp.start_listening_for_changes
|
114
|
+
end
|
115
|
+
|
116
|
+
# simulate changes
|
117
|
+
session.simulate_events(["foo.png"], [], [])
|
118
|
+
|
119
|
+
# simulate the user stopping the listening with Ctrl-C
|
120
|
+
session.interrupt
|
121
|
+
|
122
|
+
# whatever tests to make you app's callback was called:
|
123
|
+
assert_equal(%w(foo.png), myapp.updated_files_or_something)
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
## Summary
|
128
|
+
|
129
|
+
By using the above interface and the single using test above, you shouldn't
|
130
|
+
have to care about anything else related about how Listen works with your app.
|
131
|
+
|
132
|
+
|
133
|
+
## Contributing
|
134
|
+
|
135
|
+
1. Fork it ( https://github.com/[my-github-username]/listen-compat/fork )
|
136
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
137
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
138
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
139
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/example.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'listen/compat/wrapper'
|
2
|
+
|
3
|
+
listener = Listen::Compat::Wrapper.create
|
4
|
+
|
5
|
+
directories = %w(lib pkg spec)
|
6
|
+
options = { force_polling: false }
|
7
|
+
|
8
|
+
listener.listen(directories, options) do |modified, added, removed|
|
9
|
+
puts "Modified: #{modified.inspect}"
|
10
|
+
puts "Added: #{added.inspect}"
|
11
|
+
puts "Removed: #{removed.inspect}"
|
12
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'listen/compat/test/session'
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'listen/compat/wrapper'
|
2
|
+
|
3
|
+
module Listen
|
4
|
+
module Compat
|
5
|
+
module Test
|
6
|
+
class Fake < Listen::Compat::Wrapper::Common
|
7
|
+
def self.fire_events(thread, *args)
|
8
|
+
processed = _processed(thread)
|
9
|
+
processed.pop until processed.empty?
|
10
|
+
_events(thread) << args
|
11
|
+
processed.pop
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.collect_instances(thread)
|
15
|
+
return [] if _instances(thread).empty?
|
16
|
+
result = []
|
17
|
+
result << _instances(thread).pop until _instances(thread).empty?
|
18
|
+
result
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :directories
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
thread = Thread.current
|
25
|
+
|
26
|
+
thread[:fake_instances] = Queue.new
|
27
|
+
thread[:fake_events] = Queue.new
|
28
|
+
thread[:fake_processed_events] = Queue.new
|
29
|
+
|
30
|
+
Fake._instances(thread) << self
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def _start_and_wait(*args, &block)
|
36
|
+
@directories = args[0..-2]
|
37
|
+
loop do
|
38
|
+
ev = Fake._events(Thread.current).pop
|
39
|
+
block.call(*ev)
|
40
|
+
Fake._processed(Thread.current) << ev
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def _stop
|
45
|
+
end
|
46
|
+
|
47
|
+
def self._processed(thread)
|
48
|
+
thread[:fake_processed_events]
|
49
|
+
end
|
50
|
+
|
51
|
+
def self._events(thread)
|
52
|
+
thread[:fake_events]
|
53
|
+
end
|
54
|
+
|
55
|
+
def self._instances(thread)
|
56
|
+
thread[:fake_instances]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'listen/compat/wrapper'
|
2
|
+
require 'listen/compat/test/fake'
|
3
|
+
require 'listen/compat/test/simple'
|
4
|
+
|
5
|
+
module Listen
|
6
|
+
module Compat
|
7
|
+
module Test
|
8
|
+
# Class for conveniently simulating interaction with Listen
|
9
|
+
class Session
|
10
|
+
# Calls the potentially blocking given block in a background thread
|
11
|
+
def initialize(wrapper_class = nil, &block)
|
12
|
+
Wrapper.wrapper_class = wrapper_class || Listen::Compat::Test::Fake
|
13
|
+
Wrapper.listen_module = Listen::Compat::Test::Simple
|
14
|
+
|
15
|
+
fail 'No block given!' unless block_given?
|
16
|
+
|
17
|
+
@thread = Thread.new { _supervise(&block) }
|
18
|
+
end
|
19
|
+
|
20
|
+
# Simulate a Ctrl-C from the user
|
21
|
+
def interrupt
|
22
|
+
_wait_until_ready
|
23
|
+
@thread.raise Interrupt
|
24
|
+
@thread.join
|
25
|
+
end
|
26
|
+
|
27
|
+
# Simulate Listen events you want passed asynchronously to your callback
|
28
|
+
def simulate_events(modified, added, removed)
|
29
|
+
_wait_until_ready
|
30
|
+
Fake.fire_events(@thread, *_events(modified, added, removed))
|
31
|
+
end
|
32
|
+
|
33
|
+
# Return a list of fake Listen instances actually created
|
34
|
+
def instances
|
35
|
+
@instances ||= Fake.collect_instances(@thread)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def _supervise(&block)
|
41
|
+
block.call
|
42
|
+
rescue StandardError => e
|
43
|
+
msg = "\n\nERROR: Watched listen thread failed: %s: \n%s"
|
44
|
+
STDERR.puts format(msg, e.message, e.backtrace * "\n")
|
45
|
+
raise
|
46
|
+
end
|
47
|
+
|
48
|
+
def _events(modified, added, removed)
|
49
|
+
[_abs_paths(modified), _abs_paths(added), _abs_paths(removed)]
|
50
|
+
end
|
51
|
+
|
52
|
+
def _abs_paths(paths)
|
53
|
+
paths.map { |path| ::File.expand_path(path) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def _wait_until_ready
|
57
|
+
sleep 0.1
|
58
|
+
sleep 0.1 while @thread.status == 'running'
|
59
|
+
|
60
|
+
# Show error on crashes
|
61
|
+
@thread.join if @thread.status.nil?
|
62
|
+
|
63
|
+
return if @thread.status == 'sleep'
|
64
|
+
|
65
|
+
fail "Unexpected thread state: #{@thread.status.inspect}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
require 'listen/compat/version' # just for convenience
|
2
|
+
|
3
|
+
module Listen
|
4
|
+
module Compat
|
5
|
+
# Tries to require Listen using rubygems or vendored version
|
6
|
+
module Loader
|
7
|
+
module_function
|
8
|
+
|
9
|
+
def load!
|
10
|
+
defined?(gem) ? try_rubygems : try_without_rubygems
|
11
|
+
end
|
12
|
+
|
13
|
+
def try_rubygems
|
14
|
+
gem 'listen', '>= 1.1.0', '< 3.0.0'
|
15
|
+
require 'listen'
|
16
|
+
rescue LoadError, Gem::LoadError => e
|
17
|
+
e.message.replace(format("%s\n%s", e.message, msg_about_gem_install))
|
18
|
+
raise
|
19
|
+
end
|
20
|
+
|
21
|
+
def compatible_version
|
22
|
+
!older_than_193? ? '~> 2.7' : '~> 1.1'
|
23
|
+
end
|
24
|
+
|
25
|
+
def older_than_193?
|
26
|
+
Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('1.9.3')
|
27
|
+
end
|
28
|
+
|
29
|
+
def msg_about_gem_install
|
30
|
+
format("Run \"gem install listen --version '%s'\" to get it.",
|
31
|
+
compatible_version)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module Wrapper
|
36
|
+
class << self
|
37
|
+
attr_writer :listen_module
|
38
|
+
attr_accessor :wrapper_class
|
39
|
+
|
40
|
+
def listen_module
|
41
|
+
@listen_module ||= Listen
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# History of bugs/workarounds:
|
46
|
+
#
|
47
|
+
# Ancient (< 2.0.0) - very old version
|
48
|
+
# - uses start!
|
49
|
+
# - polling method
|
50
|
+
# - start method blocks
|
51
|
+
# - fails on readonly directories and files
|
52
|
+
#
|
53
|
+
# Old (>= 2.0.0) - major API change
|
54
|
+
# - uses Celluloid, so shutdown/thread handling is different
|
55
|
+
# - start returns a thread (not sure if 2.0.0 or later)
|
56
|
+
# - sleep is needed to block
|
57
|
+
# = 2.7.6 - start() returns adapter thread (instead of wait_thread)
|
58
|
+
#
|
59
|
+
# Stale (>= 2.7.7) - broke mutliple dir handling on OSX (#243)
|
60
|
+
# - devious threads hack in Sass works by accident (!)
|
61
|
+
# = 2.7.11 - last version where start still returns a thread
|
62
|
+
#
|
63
|
+
# Current (>= 2.7.12)- start() no longer returns a thread
|
64
|
+
# - fixed multiple dir handling (#243)
|
65
|
+
# = 2.8.0 - current version
|
66
|
+
|
67
|
+
# "Expected" functionality from any Listen version
|
68
|
+
class Common
|
69
|
+
# Run listen continously to monitor changes and gracefully terminate
|
70
|
+
# on Ctrl-C
|
71
|
+
def listen(*args, &block)
|
72
|
+
_start_and_wait(*args, &block)
|
73
|
+
rescue Interrupt
|
74
|
+
_stop
|
75
|
+
end
|
76
|
+
|
77
|
+
protected
|
78
|
+
|
79
|
+
# Overridable method so a fake implementation can be used in tests
|
80
|
+
def _listen_module
|
81
|
+
Wrapper.listen_module
|
82
|
+
end
|
83
|
+
|
84
|
+
# Overridable method so a fake implementation can be used in tests
|
85
|
+
def _listen_class
|
86
|
+
_listen_module::Listener
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
# Run listen continuously, regardless whether it blocks or starts
|
92
|
+
# a background thread
|
93
|
+
def _start_and_wait(*args, &block)
|
94
|
+
_listen_module.to(*args, &block).start
|
95
|
+
sleep
|
96
|
+
end
|
97
|
+
|
98
|
+
# Gracefully shutdown Listen after a Ctrl-C, join threads, etc.
|
99
|
+
def _stop
|
100
|
+
_listen_module.stop
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Workarounds for pre 2.0 versions of Listen
|
105
|
+
class Ancient < Common
|
106
|
+
NEXT_VERSION = Gem::Version.new('2.0.0')
|
107
|
+
|
108
|
+
# A Listen version prior to 2.0 will write a test file to a directory
|
109
|
+
# to see if a watcher supports watching that directory. That breaks
|
110
|
+
# horribly on read-only directories, so we filter those out.
|
111
|
+
def watchable_directories(directories)
|
112
|
+
directories.select { |d| ::File.directory?(d) && ::File.writable?(d) }
|
113
|
+
end
|
114
|
+
|
115
|
+
def listen(*args, &block)
|
116
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
117
|
+
# Note: force_polling is a method here (since Listen 2.0.0 it's an
|
118
|
+
# option passed to Listen.new)
|
119
|
+
poll = options[:force_polling]
|
120
|
+
|
121
|
+
directories = watchable_directories(args.flatten)
|
122
|
+
|
123
|
+
# Don't optimize this out because of Ruby 1.8
|
124
|
+
args = directories
|
125
|
+
args << options
|
126
|
+
|
127
|
+
listener = _listen_class.new(*args, &block)
|
128
|
+
listener.force_polling(true) if poll
|
129
|
+
listener.start!
|
130
|
+
rescue Interrupt
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# >= 2.0.0, <= 2.7.6
|
135
|
+
class Old < Common
|
136
|
+
NEXT_VERSION = Gem::Version.new('2.7.7')
|
137
|
+
end
|
138
|
+
|
139
|
+
# >= 2.7.7, <= 2.7.11
|
140
|
+
class Stale < Common
|
141
|
+
NEXT_VERSION = Gem::Version.new('2.7.12')
|
142
|
+
|
143
|
+
# Work around guard/listen#243 (>= v2.7.9, < v2.8.0)
|
144
|
+
def _start_and_wait(*args, &block)
|
145
|
+
options = args.pop if args.last.is_a?(Hash)
|
146
|
+
listeners = args.map do |dir|
|
147
|
+
_listen_module.to(dir, options, &block)
|
148
|
+
end
|
149
|
+
listeners.map(&:start)
|
150
|
+
sleep
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# >= 2.7.12, <= 2.8.0
|
155
|
+
class Current < Common
|
156
|
+
NEXT_VERSION = Gem::Version.new('2.99.99')
|
157
|
+
end
|
158
|
+
|
159
|
+
# Returns a wrapper matching the listen version
|
160
|
+
# @param version_string overrides detection (e.g. for testing)
|
161
|
+
def self.create(version_string = nil)
|
162
|
+
return Wrapper.wrapper_class.new if Wrapper.wrapper_class
|
163
|
+
|
164
|
+
version = Gem::Version.new(version_string || _detect_listen_version)
|
165
|
+
|
166
|
+
[Ancient, Old, Stale, Current].each do |klass|
|
167
|
+
return klass.new if version < klass.const_get('NEXT_VERSION')
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
def self._detect_listen_version
|
174
|
+
Loader.load!
|
175
|
+
require 'listen/version'
|
176
|
+
Listen::VERSION
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'listen/compat/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'listen-compat'
|
8
|
+
spec.version = Listen::Compat::VERSION
|
9
|
+
spec.authors = ['Cezary Baginski']
|
10
|
+
spec.email = ['cezary@chronomantic.net']
|
11
|
+
spec.summary = 'Simplified compatibility layer for Listen gem'
|
12
|
+
spec.description = 'For developers to have a minimal, guaranteed API for \
|
13
|
+
using Listen'
|
14
|
+
|
15
|
+
spec.homepage = 'https://github.com/guard/listen-compat'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0")
|
19
|
+
spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
|
20
|
+
spec.test_files = spec.files.grep(/^(test|spec|features)\//)
|
21
|
+
spec.require_paths = ['lib']
|
22
|
+
|
23
|
+
spec.add_development_dependency 'bundler', '~> 1.7'
|
24
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
25
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'listen/compat/test/session'
|
2
|
+
|
3
|
+
class MyExampleApp
|
4
|
+
attr_reader :changed
|
5
|
+
|
6
|
+
def start_listening_for_changes
|
7
|
+
listener = Listen::Compat::Wrapper.create
|
8
|
+
|
9
|
+
directories = %w(foo bar baz)
|
10
|
+
options = { force_polling: false }
|
11
|
+
|
12
|
+
listener.listen(directories, options) do |modified, _added, _removed|
|
13
|
+
@changed ||= []
|
14
|
+
@changed += modified.map do |full_path|
|
15
|
+
Pathname(full_path).relative_path_from(Pathname.pwd).to_s
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
RSpec.describe MyExampleApp do
|
22
|
+
it 'works' do
|
23
|
+
myapp = MyExampleApp.new
|
24
|
+
|
25
|
+
session = Listen::Compat::Test::Session.new do
|
26
|
+
# put whatever code would cause Listen to block here:
|
27
|
+
myapp.start_listening_for_changes
|
28
|
+
end
|
29
|
+
|
30
|
+
# simulate changes
|
31
|
+
session.simulate_events(['foo.png'], [], [])
|
32
|
+
|
33
|
+
# simulate the user stopping the listening with Ctrl-C
|
34
|
+
session.interrupt
|
35
|
+
|
36
|
+
# whatever tests to make you app's callback was called:
|
37
|
+
expect(myapp.changed).to eq(%w(foo.png))
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,228 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'pathname'
|
3
|
+
require 'tmpdir'
|
4
|
+
|
5
|
+
require 'listen/compat/wrapper'
|
6
|
+
|
7
|
+
# TODO: since we're using RSpec now, this is not necessary
|
8
|
+
module MockListen
|
9
|
+
class Listener
|
10
|
+
def initialize(*args)
|
11
|
+
@calls = Queue.new
|
12
|
+
@calls << [__method__, *args]
|
13
|
+
MockListen.add_instance(self)
|
14
|
+
end
|
15
|
+
|
16
|
+
def method_missing(meth, *args, &_block)
|
17
|
+
@calls << [meth, *args]
|
18
|
+
end
|
19
|
+
|
20
|
+
def calls
|
21
|
+
MockListen.dump(@calls)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class << self
|
26
|
+
def dump(queue)
|
27
|
+
res = []
|
28
|
+
res << queue.pop until queue.empty?
|
29
|
+
res
|
30
|
+
end
|
31
|
+
|
32
|
+
def setup_for_tests
|
33
|
+
Listen::Compat::Wrapper.listen_module = MockListen
|
34
|
+
Listen::Compat::Wrapper.wrapper_class = nil # autodetect
|
35
|
+
@calls = Queue.new
|
36
|
+
@mocks = Queue.new
|
37
|
+
@responses = {}
|
38
|
+
@called = []
|
39
|
+
end
|
40
|
+
|
41
|
+
def reset_for_tests
|
42
|
+
instance_variables.each do |var|
|
43
|
+
instance_variable_set(var, :unset)
|
44
|
+
end
|
45
|
+
Listen::Compat::Wrapper.listen_module = nil
|
46
|
+
Listen::Compat::Wrapper.wrapper_class = nil
|
47
|
+
end
|
48
|
+
|
49
|
+
# setup actions
|
50
|
+
attr_reader :responses
|
51
|
+
|
52
|
+
def add_instance(obj)
|
53
|
+
@mocks << obj
|
54
|
+
end
|
55
|
+
|
56
|
+
# Get results
|
57
|
+
def instances
|
58
|
+
MockListen.dump(@mocks)
|
59
|
+
end
|
60
|
+
|
61
|
+
def calls
|
62
|
+
MockListen.dump(@calls)
|
63
|
+
end
|
64
|
+
|
65
|
+
def method_missing(meth, *args, &block)
|
66
|
+
@calls << [meth, *args]
|
67
|
+
block = responses[meth]
|
68
|
+
block.call(*args) unless block.nil?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
module DelayedInterruptHelper
|
74
|
+
def delayed_interrupt(&block)
|
75
|
+
th = Thread.new(&block)
|
76
|
+
sleep 0.1
|
77
|
+
sleep 0.1 while (status = th.status) == 'running'
|
78
|
+
th.raise Interrupt
|
79
|
+
th.join
|
80
|
+
status
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
RSpec.describe Listen::Compat::Wrapper::Ancient do
|
85
|
+
let(:wrapper) { Listen::Compat::Wrapper.create('0.1.0') }
|
86
|
+
|
87
|
+
before do
|
88
|
+
MockListen.setup_for_tests
|
89
|
+
MockListen.responses[:start!] = proc { fail Interrupt }
|
90
|
+
end
|
91
|
+
|
92
|
+
after do
|
93
|
+
MockListen.reset_for_tests
|
94
|
+
end
|
95
|
+
|
96
|
+
it { is_expected.to be_a described_class }
|
97
|
+
|
98
|
+
it 'readonly dirs are avoided' do
|
99
|
+
tmpdir, result = Dir.mktmpdir do |dir|
|
100
|
+
ro_dir = File.join(dir, 'foo')
|
101
|
+
FileUtils.mkdir(ro_dir, mode: 0444)
|
102
|
+
[dir, wrapper.watchable_directories([dir, ro_dir, '.'])]
|
103
|
+
end
|
104
|
+
|
105
|
+
expect(result).to eq([tmpdir, '.'])
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'passes parameters to listen' do
|
109
|
+
wrapper.listen('.', {})
|
110
|
+
expect(MockListen.instances[0].calls[0]).to eq([:initialize, '.', {}])
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'calls start' do
|
114
|
+
wrapper.listen('.', {})
|
115
|
+
expect(MockListen.instances[0].calls[1]).to eq([:start!])
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'does not call stop' do
|
119
|
+
wrapper.listen('.', {})
|
120
|
+
expect(MockListen.calls).to eq([])
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
RSpec.describe Listen::Compat::Wrapper::Old do
|
125
|
+
include DelayedInterruptHelper
|
126
|
+
|
127
|
+
let(:wrapper) { Listen::Compat::Wrapper.create('2.7.6') }
|
128
|
+
|
129
|
+
before do
|
130
|
+
MockListen.setup_for_tests
|
131
|
+
MockListen.responses[:to] = proc { |*args| MockListen::Listener.new(*args) }
|
132
|
+
end
|
133
|
+
|
134
|
+
after do
|
135
|
+
MockListen.reset_for_tests
|
136
|
+
end
|
137
|
+
|
138
|
+
it { is_expected.to be_a described_class }
|
139
|
+
|
140
|
+
it 'passes parameters to listen' do
|
141
|
+
delayed_interrupt { wrapper.listen('.', {}) }
|
142
|
+
expect(MockListen.calls[0]).to eq([:to, '.', {}])
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'calls start' do
|
146
|
+
delayed_interrupt { wrapper.listen('.', {}) }
|
147
|
+
expect(MockListen.instances[0].calls[1]).to eq([:start])
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'sleeps after start' do
|
151
|
+
status = delayed_interrupt { wrapper.listen('.', {}) }
|
152
|
+
expect(status).to eq('sleep')
|
153
|
+
end
|
154
|
+
|
155
|
+
it 'calls stop' do
|
156
|
+
delayed_interrupt { wrapper.listen('.', {}) }
|
157
|
+
expect(MockListen.calls[1]).to eq([:stop])
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
RSpec.describe Listen::Compat::Wrapper::Stale do
|
162
|
+
include DelayedInterruptHelper
|
163
|
+
|
164
|
+
let(:wrapper) { Listen::Compat::Wrapper.create('2.7.11') }
|
165
|
+
|
166
|
+
before do
|
167
|
+
MockListen.setup_for_tests
|
168
|
+
MockListen.responses[:to] = proc { |*args| MockListen::Listener.new(*args) }
|
169
|
+
end
|
170
|
+
|
171
|
+
after do
|
172
|
+
MockListen.reset_for_tests
|
173
|
+
end
|
174
|
+
|
175
|
+
it { is_expected.to be_a described_class }
|
176
|
+
|
177
|
+
it 'calls start' do
|
178
|
+
delayed_interrupt { wrapper.listen('.', {}) }
|
179
|
+
expect(MockListen.instances[0].calls[1]).to eq([:start])
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'sleeps after start' do
|
183
|
+
status = delayed_interrupt { wrapper.listen('.', {}) }
|
184
|
+
expect(status).to eq('sleep')
|
185
|
+
end
|
186
|
+
|
187
|
+
it 'calls stop' do
|
188
|
+
delayed_interrupt { wrapper.listen('.', {}) }
|
189
|
+
expect(MockListen.calls[1]).to eq([:stop])
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
RSpec.describe Listen::Compat::Wrapper::Current do
|
194
|
+
include DelayedInterruptHelper
|
195
|
+
|
196
|
+
let(:wrapper) { Listen::Compat::Wrapper.create('2.7.12') }
|
197
|
+
|
198
|
+
before do
|
199
|
+
MockListen.setup_for_tests
|
200
|
+
MockListen.responses[:to] = proc { |*args| MockListen::Listener.new(*args) }
|
201
|
+
end
|
202
|
+
|
203
|
+
after do
|
204
|
+
MockListen.reset_for_tests
|
205
|
+
end
|
206
|
+
|
207
|
+
it { is_expected.to be_a described_class }
|
208
|
+
|
209
|
+
it 'passes parameters to listen' do
|
210
|
+
delayed_interrupt { wrapper.listen('.', {}) }
|
211
|
+
expect(MockListen.calls[0]).to eq([:to, '.', {}])
|
212
|
+
end
|
213
|
+
|
214
|
+
it 'calls start' do
|
215
|
+
delayed_interrupt { wrapper.listen('.', {}) }
|
216
|
+
expect(MockListen.instances[0].calls[1]).to eq([:start])
|
217
|
+
end
|
218
|
+
|
219
|
+
it 'sleeps after start' do
|
220
|
+
status = delayed_interrupt { wrapper.listen('.', {}) }
|
221
|
+
expect(status).to eq('sleep')
|
222
|
+
end
|
223
|
+
|
224
|
+
it 'calls stop' do
|
225
|
+
delayed_interrupt { wrapper.listen('.', {}) }
|
226
|
+
expect(MockListen.calls[1]).to eq([:stop])
|
227
|
+
end
|
228
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# The `.rspec` file also contains a few flags that are not defaults but that
|
2
|
+
# users commonly want.
|
3
|
+
#
|
4
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
5
|
+
RSpec.configure do |config|
|
6
|
+
# rspec-expectations config goes here. You can use an alternate
|
7
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
8
|
+
# assertions if you prefer.
|
9
|
+
config.expect_with :rspec do |expectations|
|
10
|
+
# This option will default to `true` in RSpec 4. It makes the `description`
|
11
|
+
# and `failure_message` of custom matchers include text for helper methods
|
12
|
+
# defined using `chain`, e.g.:
|
13
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
14
|
+
# # => "be bigger than 2 and smaller than 4"
|
15
|
+
# ...rather than:
|
16
|
+
# # => "be bigger than 2"
|
17
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
18
|
+
end
|
19
|
+
|
20
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
21
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
22
|
+
config.mock_with :rspec do |mocks|
|
23
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
24
|
+
# a real object. This is generally recommended, and will default to
|
25
|
+
# `true` in RSpec 4.
|
26
|
+
mocks.verify_doubled_constant_names = true
|
27
|
+
mocks.verify_partial_doubles = true
|
28
|
+
end
|
29
|
+
|
30
|
+
# The settings below are suggested to provide a good initial experience
|
31
|
+
# with RSpec, but feel free to customize to your heart's content.
|
32
|
+
|
33
|
+
# These two settings work together to allow you to limit a spec run
|
34
|
+
# to individual examples or groups you care about by tagging them with
|
35
|
+
# `:focus` metadata. When nothing is tagged with `:focus`, all examples
|
36
|
+
# get run.
|
37
|
+
config.filter_run focus: ENV['CI'] != 'true'
|
38
|
+
|
39
|
+
config.run_all_when_everything_filtered = true
|
40
|
+
|
41
|
+
config.disable_monkey_patching!
|
42
|
+
|
43
|
+
# This setting enables warnings. It's recommended, but in some cases may
|
44
|
+
# be too noisy due to issues in dependencies.
|
45
|
+
config.warnings = true
|
46
|
+
|
47
|
+
# Many RSpec users commonly either run the entire suite or an individual
|
48
|
+
# file, and it's useful to allow more verbose output when running an
|
49
|
+
# individual spec file.
|
50
|
+
if config.files_to_run.one?
|
51
|
+
# Use the documentation formatter for detailed output,
|
52
|
+
# unless a formatter has already been configured
|
53
|
+
# (e.g. via a command-line flag).
|
54
|
+
config.default_formatter = 'doc'
|
55
|
+
end
|
56
|
+
|
57
|
+
# Print the 10 slowest examples and example groups at the
|
58
|
+
# end of the spec run, to help surface which specs are running
|
59
|
+
# particularly slow.
|
60
|
+
# config.profile_examples = 10
|
61
|
+
|
62
|
+
# Run specs in random order to surface order dependencies. If you find an
|
63
|
+
# order dependency and want to debug it, you can fix the order by providing
|
64
|
+
# the seed, which is printed after each run.
|
65
|
+
# --seed 1234
|
66
|
+
config.order = :random
|
67
|
+
|
68
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
69
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
70
|
+
# test failures related to randomization by passing the same `--seed` value
|
71
|
+
# as the one that triggered the failure.
|
72
|
+
Kernel.srand config.seed
|
73
|
+
end
|
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: listen-compat
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Cezary Baginski
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-11-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.7'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
description: |-
|
42
|
+
For developers to have a minimal, guaranteed API for \
|
43
|
+
using Listen
|
44
|
+
email:
|
45
|
+
- cezary@chronomantic.net
|
46
|
+
executables: []
|
47
|
+
extensions: []
|
48
|
+
extra_rdoc_files: []
|
49
|
+
files:
|
50
|
+
- ".gitignore"
|
51
|
+
- ".rspec"
|
52
|
+
- ".rubocop.yml"
|
53
|
+
- ".rubocop_todo.yml"
|
54
|
+
- Gemfile
|
55
|
+
- LICENSE.txt
|
56
|
+
- README.md
|
57
|
+
- Rakefile
|
58
|
+
- example.rb
|
59
|
+
- lib/listen/compat.rb
|
60
|
+
- lib/listen/compat/test.rb
|
61
|
+
- lib/listen/compat/test/fake.rb
|
62
|
+
- lib/listen/compat/test/session.rb
|
63
|
+
- lib/listen/compat/test/simple.rb
|
64
|
+
- lib/listen/compat/version.rb
|
65
|
+
- lib/listen/compat/wrapper.rb
|
66
|
+
- listen-compat.gemspec
|
67
|
+
- spec/listen/compat/example_spec.rb
|
68
|
+
- spec/listen/compat/wrapper_spec.rb
|
69
|
+
- spec/spec_helper.rb
|
70
|
+
homepage: https://github.com/guard/listen-compat
|
71
|
+
licenses:
|
72
|
+
- MIT
|
73
|
+
metadata: {}
|
74
|
+
post_install_message:
|
75
|
+
rdoc_options: []
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
requirements: []
|
89
|
+
rubyforge_project:
|
90
|
+
rubygems_version: 2.2.2
|
91
|
+
signing_key:
|
92
|
+
specification_version: 4
|
93
|
+
summary: Simplified compatibility layer for Listen gem
|
94
|
+
test_files:
|
95
|
+
- spec/listen/compat/example_spec.rb
|
96
|
+
- spec/listen/compat/wrapper_spec.rb
|
97
|
+
- spec/spec_helper.rb
|