spring 0.0.7 → 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|