spring 0.0.7 → 0.0.8
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.
- data/CHANGELOG.md +11 -0
- data/Gemfile +4 -0
- data/README.md +49 -24
- data/lib/spring/application.rb +30 -30
- data/lib/spring/application_manager.rb +34 -15
- data/lib/spring/client.rb +9 -7
- data/lib/spring/client/binstub.rb +8 -3
- data/lib/spring/client/help.rb +20 -32
- data/lib/spring/client/rails.rb +29 -0
- data/lib/spring/client/run.rb +55 -51
- data/lib/spring/client/start.rb +17 -0
- data/lib/spring/client/status.rb +1 -1
- data/lib/spring/client/stop.rb +1 -1
- data/lib/spring/commands.rb +36 -20
- data/lib/spring/errors.rb +3 -0
- data/lib/spring/server.rb +36 -12
- data/lib/spring/version.rb +1 -1
- data/lib/spring/watcher.rb +28 -0
- data/lib/spring/watcher/abstract.rb +83 -0
- data/lib/spring/watcher/listen.rb +54 -0
- data/lib/spring/watcher/polling.rb +59 -0
- data/test/acceptance/app_test.rb +62 -32
- data/test/apps/rails-3-2/Gemfile +5 -0
- data/test/unit/client/help_test.rb +27 -19
- data/test/unit/commands_test.rb +26 -1
- data/test/unit/watcher_test.rb +171 -0
- metadata +12 -6
- data/lib/spring/application_watcher.rb +0 -43
- data/test/unit/application_watcher_test.rb +0 -67
data/lib/spring/version.rb
CHANGED
@@ -0,0 +1,28 @@
|
|
1
|
+
require "spring/watcher/abstract"
|
2
|
+
require "spring/watcher/listen"
|
3
|
+
require "spring/watcher/polling"
|
4
|
+
|
5
|
+
module Spring
|
6
|
+
class << self
|
7
|
+
attr_accessor :watch_interval
|
8
|
+
attr_writer :watcher
|
9
|
+
end
|
10
|
+
|
11
|
+
self.watch_interval = 0.2
|
12
|
+
|
13
|
+
def self.watcher
|
14
|
+
@watcher ||= watcher_class.new(Spring.application_root_path, watch_interval)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.watcher_class
|
18
|
+
if Watcher::Listen.available?
|
19
|
+
Watcher::Listen
|
20
|
+
else
|
21
|
+
Watcher::Polling
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.watch(*items)
|
26
|
+
watcher.add *items
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require "set"
|
2
|
+
require "pathname"
|
3
|
+
require "mutex_m"
|
4
|
+
|
5
|
+
module Spring
|
6
|
+
module Watcher
|
7
|
+
# A user of a watcher can use IO.select to wait for changes:
|
8
|
+
#
|
9
|
+
# watcher = MyWatcher.new(root, latency)
|
10
|
+
# IO.select([watcher]) # watcher is running in background
|
11
|
+
# watcher.stale? # => true
|
12
|
+
class Abstract
|
13
|
+
include Mutex_m
|
14
|
+
|
15
|
+
attr_reader :files, :directories, :root, :latency
|
16
|
+
|
17
|
+
def initialize(root, latency)
|
18
|
+
super()
|
19
|
+
|
20
|
+
@root = File.realpath(root)
|
21
|
+
@latency = latency
|
22
|
+
@files = Set.new
|
23
|
+
@directories = Set.new
|
24
|
+
@stale = false
|
25
|
+
@io_listener = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def add(*items)
|
29
|
+
items = items.flatten.map do |item|
|
30
|
+
item = Pathname.new(item)
|
31
|
+
|
32
|
+
if item.relative?
|
33
|
+
Pathname.new("#{root}/#{item}")
|
34
|
+
else
|
35
|
+
item
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
items.each do |item|
|
40
|
+
if item.directory?
|
41
|
+
directories << item.realpath.to_s
|
42
|
+
else
|
43
|
+
files << item.realpath.to_s
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
subjects_changed
|
48
|
+
end
|
49
|
+
|
50
|
+
def stale?
|
51
|
+
@stale
|
52
|
+
end
|
53
|
+
|
54
|
+
def mark_stale
|
55
|
+
@stale = true
|
56
|
+
@io_listener.write "." if @io_listener
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_io
|
60
|
+
read, write = IO.pipe
|
61
|
+
@io_listener = write
|
62
|
+
read
|
63
|
+
end
|
64
|
+
|
65
|
+
def restart
|
66
|
+
stop
|
67
|
+
start
|
68
|
+
end
|
69
|
+
|
70
|
+
def start
|
71
|
+
raise NotImplementedError
|
72
|
+
end
|
73
|
+
|
74
|
+
def stop
|
75
|
+
raise NotImplementedError
|
76
|
+
end
|
77
|
+
|
78
|
+
def subjects_changed
|
79
|
+
raise NotImplementedError
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Spring
|
2
|
+
module Watcher
|
3
|
+
class Listen < Abstract
|
4
|
+
attr_reader :listener
|
5
|
+
|
6
|
+
def self.available?
|
7
|
+
require "listen"
|
8
|
+
true
|
9
|
+
rescue LoadError
|
10
|
+
false
|
11
|
+
end
|
12
|
+
|
13
|
+
def start
|
14
|
+
unless @listener
|
15
|
+
@listener = ::Listen::MultiListener.new(*base_directories)
|
16
|
+
@listener.latency(latency)
|
17
|
+
@listener.change(&method(:changed))
|
18
|
+
@listener.start(false)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def stop
|
23
|
+
if @listener
|
24
|
+
@listener.stop
|
25
|
+
@listener = nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def subjects_changed
|
30
|
+
if @listener && @listener.directories.sort != base_directories.sort
|
31
|
+
restart
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def watching?(file)
|
36
|
+
files.include?(file) || file.start_with?(*directories)
|
37
|
+
end
|
38
|
+
|
39
|
+
def changed(modified, added, removed)
|
40
|
+
synchronize do
|
41
|
+
if (modified + added + removed).any? { |f| watching? f }
|
42
|
+
mark_stale
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def base_directories
|
48
|
+
[root] +
|
49
|
+
files.reject { |f| f.start_with? root }.map { |f| File.expand_path("#{f}/..") } +
|
50
|
+
directories.reject { |d| d.start_with? root }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Spring
|
2
|
+
module Watcher
|
3
|
+
class Polling < Abstract
|
4
|
+
attr_reader :mtime
|
5
|
+
|
6
|
+
def initialize(root, latency)
|
7
|
+
super
|
8
|
+
@mtime = nil
|
9
|
+
@poller = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def check_stale
|
13
|
+
synchronize { mark_stale if mtime < compute_mtime }
|
14
|
+
end
|
15
|
+
|
16
|
+
def add(*)
|
17
|
+
check_stale if @poller
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
def start
|
22
|
+
unless @poller
|
23
|
+
@poller = Thread.new {
|
24
|
+
Thread.current.abort_on_exception = true
|
25
|
+
|
26
|
+
loop do
|
27
|
+
Kernel.sleep latency
|
28
|
+
check_stale
|
29
|
+
end
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def stop
|
35
|
+
if @poller
|
36
|
+
@poller.kill
|
37
|
+
@poller = nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def subjects_changed
|
42
|
+
@mtime = compute_mtime
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def compute_mtime
|
48
|
+
expanded_files.map { |f| File.mtime(f).to_f }.max || 0
|
49
|
+
rescue Errno::ENOENT
|
50
|
+
# if a file does no longer exist, the watcher is always stale.
|
51
|
+
Float::MAX
|
52
|
+
end
|
53
|
+
|
54
|
+
def expanded_files
|
55
|
+
files + Dir["{#{directories.map { |d| "#{d}/**/*" }.join(",")}}"]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/test/acceptance/app_test.rb
CHANGED
@@ -44,10 +44,10 @@ class AppTest < ActiveSupport::TestCase
|
|
44
44
|
)
|
45
45
|
end
|
46
46
|
|
47
|
-
_, status = Timeout.timeout(opts.fetch(:timeout,
|
47
|
+
_, status = Timeout.timeout(opts.fetch(:timeout, 10)) { Process.wait2 }
|
48
48
|
|
49
49
|
stdout, stderr = read_streams
|
50
|
-
puts dump_streams(stdout, stderr) if ENV["SPRING_DEBUG"]
|
50
|
+
puts dump_streams(command, stdout, stderr) if ENV["SPRING_DEBUG"]
|
51
51
|
|
52
52
|
@times << (Time.now - start_time) if @times
|
53
53
|
|
@@ -57,7 +57,7 @@ class AppTest < ActiveSupport::TestCase
|
|
57
57
|
stderr: stderr,
|
58
58
|
}
|
59
59
|
rescue Timeout::Error => e
|
60
|
-
raise e, "Output:\n\n#{dump_streams(*read_streams)}"
|
60
|
+
raise e, "Output:\n\n#{dump_streams(command, *read_streams)}"
|
61
61
|
end
|
62
62
|
|
63
63
|
def read_streams
|
@@ -68,11 +68,19 @@ class AppTest < ActiveSupport::TestCase
|
|
68
68
|
end
|
69
69
|
end
|
70
70
|
|
71
|
-
def dump_streams(stdout, stderr)
|
72
|
-
output = "
|
73
|
-
|
74
|
-
|
75
|
-
|
71
|
+
def dump_streams(command, stdout, stderr)
|
72
|
+
output = "$ #{command}\n"
|
73
|
+
|
74
|
+
unless stdout.chomp.empty?
|
75
|
+
output << "--- stdout ---\n"
|
76
|
+
output << "#{stdout.chomp}\n"
|
77
|
+
end
|
78
|
+
|
79
|
+
unless stderr.chomp.empty?
|
80
|
+
output << "--- stderr ---\n"
|
81
|
+
output << "#{stderr.chomp}\n"
|
82
|
+
end
|
83
|
+
|
76
84
|
output << "\n"
|
77
85
|
output
|
78
86
|
end
|
@@ -108,7 +116,7 @@ class AppTest < ActiveSupport::TestCase
|
|
108
116
|
end
|
109
117
|
|
110
118
|
def spring_test_command
|
111
|
-
"#{spring}
|
119
|
+
"#{spring} testunit #{@test}"
|
112
120
|
end
|
113
121
|
|
114
122
|
@@installed = false
|
@@ -182,29 +190,47 @@ class AppTest < ActiveSupport::TestCase
|
|
182
190
|
end
|
183
191
|
end
|
184
192
|
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
application_contents = File.read(application)
|
193
|
+
def assert_app_reloaded
|
194
|
+
application = "#{app_root}/config/application.rb"
|
195
|
+
application_contents = File.read(application)
|
189
196
|
|
190
|
-
|
197
|
+
assert_success spring_test_command
|
191
198
|
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
end
|
199
|
+
File.write(application, application_contents + <<-CODE)
|
200
|
+
class Foo
|
201
|
+
def self.omg
|
202
|
+
raise "omg"
|
197
203
|
end
|
198
|
-
|
199
|
-
|
204
|
+
end
|
205
|
+
CODE
|
206
|
+
File.write(@test, @test_contents.sub("get :index", "Foo.omg"))
|
200
207
|
|
201
|
-
|
208
|
+
await_reload
|
202
209
|
|
203
|
-
|
204
|
-
|
205
|
-
|
210
|
+
assert_speedup do
|
211
|
+
2.times { assert_failure spring_test_command, stdout: "RuntimeError: omg" }
|
212
|
+
end
|
213
|
+
ensure
|
214
|
+
File.write(application, application_contents)
|
215
|
+
end
|
216
|
+
|
217
|
+
test "app gets reloaded when preloaded files change (polling watcher)" do
|
218
|
+
assert_success "#{spring} rails runner 'puts Spring.watcher.class'", stdout: "Polling"
|
219
|
+
assert_app_reloaded
|
220
|
+
end
|
221
|
+
|
222
|
+
test "app gets reloaded when preloaded files change (listen watcher)" do
|
223
|
+
begin
|
224
|
+
gemfile = app_root.join("Gemfile")
|
225
|
+
gemfile_contents = gemfile.read
|
226
|
+
File.write(gemfile, gemfile_contents.sub(%{# gem 'listen'}, %{gem 'listen'}))
|
227
|
+
app_run "bundle install", timeout: nil
|
228
|
+
|
229
|
+
assert_success "#{spring} rails runner 'puts Spring.watcher.class'", stdout: "Listen"
|
230
|
+
assert_app_reloaded
|
206
231
|
ensure
|
207
|
-
File.write(
|
232
|
+
File.write(gemfile, gemfile_contents)
|
233
|
+
assert_success "bundle check"
|
208
234
|
end
|
209
235
|
end
|
210
236
|
|
@@ -241,14 +267,12 @@ class AppTest < ActiveSupport::TestCase
|
|
241
267
|
assert_success "#{spring} custom", stdout: "omg"
|
242
268
|
end
|
243
269
|
|
244
|
-
test "runner alias" do
|
245
|
-
assert_success "#{spring} r 'puts 1'", stdout: "1"
|
246
|
-
end
|
247
|
-
|
248
270
|
test "binstubs" do
|
249
271
|
app_run "#{spring} binstub rake"
|
272
|
+
app_run "#{spring} binstub rails"
|
250
273
|
assert_success "bin/spring help"
|
251
274
|
assert_success "bin/rake -T", stdout: "rake db:migrate"
|
275
|
+
assert_success "bin/rails runner 'puts %(omg)'", stdout: "omg"
|
252
276
|
end
|
253
277
|
|
254
278
|
test "after fork callback" do
|
@@ -257,7 +281,7 @@ class AppTest < ActiveSupport::TestCase
|
|
257
281
|
config_contents = File.read(config_path)
|
258
282
|
|
259
283
|
File.write(config_path, config_contents + "\nSpring.after_fork { puts '!callback!' }")
|
260
|
-
assert_success "#{spring}
|
284
|
+
assert_success "#{spring} rails runner 'puts 2'", stdout: "!callback!\n2"
|
261
285
|
ensure
|
262
286
|
File.write(config_path, config_contents)
|
263
287
|
end
|
@@ -278,7 +302,13 @@ class AppTest < ActiveSupport::TestCase
|
|
278
302
|
|
279
303
|
test "status" do
|
280
304
|
assert_success "#{spring} status", stdout: "Spring is not running"
|
281
|
-
app_run "#{spring} runner ''"
|
305
|
+
app_run "#{spring} rails runner ''"
|
282
306
|
assert_success "#{spring} status", stdout: "Spring is running"
|
283
307
|
end
|
308
|
+
|
309
|
+
test "runner command sets Rails environment from command-line options" do
|
310
|
+
# Not using "test" environment here to avoid false positives on Travis (where "test" is default)
|
311
|
+
assert_success "#{spring} rails runner -e staging 'puts Rails.env'", stdout: "staging"
|
312
|
+
assert_success "#{spring} rails runner --environment=staging 'puts Rails.env'", stdout: "staging"
|
313
|
+
end
|
284
314
|
end
|
data/test/apps/rails-3-2/Gemfile
CHANGED
@@ -4,28 +4,35 @@ require "spring/client/command"
|
|
4
4
|
require 'spring/client/help'
|
5
5
|
require 'spring/client'
|
6
6
|
|
7
|
-
class RandomSpringCommand
|
8
|
-
def self.description
|
9
|
-
'Random Spring Command'
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
class RandomApplicationCommand
|
14
|
-
def description
|
15
|
-
'Random Application Command'
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
7
|
class HelpTest < ActiveSupport::TestCase
|
20
8
|
def spring_commands
|
21
|
-
{
|
9
|
+
{
|
10
|
+
'command' => Class.new {
|
11
|
+
def self.description
|
12
|
+
'Random Spring Command'
|
13
|
+
end
|
14
|
+
},
|
15
|
+
'rails' => Class.new {
|
16
|
+
def self.description
|
17
|
+
"omg"
|
18
|
+
end
|
19
|
+
}
|
20
|
+
}
|
22
21
|
end
|
23
22
|
|
24
23
|
def application_commands
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
24
|
+
{
|
25
|
+
'random' => Class.new {
|
26
|
+
def description
|
27
|
+
'Random Application Command'
|
28
|
+
end
|
29
|
+
}.new,
|
30
|
+
'hidden' => Class.new {
|
31
|
+
def description
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
}.new
|
35
|
+
}
|
29
36
|
end
|
30
37
|
|
31
38
|
def setup
|
@@ -38,11 +45,12 @@ Usage: spring COMMAND [ARGS]
|
|
38
45
|
|
39
46
|
Commands for spring itself:
|
40
47
|
|
41
|
-
command
|
48
|
+
command Random Spring Command
|
42
49
|
|
43
50
|
Commands for your application:
|
44
51
|
|
45
|
-
|
52
|
+
rails omg
|
53
|
+
random Random Application Command
|
46
54
|
EOF
|
47
55
|
|
48
56
|
assert_equal expected_output.chomp, @help.formatted_help
|