listen-compat 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|