spring 0.0.7 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,3 @@
1
1
  module Spring
2
- VERSION = "0.0.7"
2
+ VERSION = "0.0.8"
3
3
  end
@@ -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
@@ -44,10 +44,10 @@ class AppTest < ActiveSupport::TestCase
44
44
  )
45
45
  end
46
46
 
47
- _, status = Timeout.timeout(opts.fetch(:timeout, 5)) { Process.wait2 }
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 = "--- stdout ---\n"
73
- output << "#{stdout.chomp}\n"
74
- output << "--- stderr ---\n"
75
- output << "#{stderr.chomp}\n"
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} test #{@test}"
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
- test "app gets reloaded when preloaded files change" do
186
- begin
187
- application = "#{app_root}/config/application.rb"
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
- assert_success spring_test_command
197
+ assert_success spring_test_command
191
198
 
192
- File.write(application, application_contents + <<-CODE)
193
- class Foo
194
- def self.omg
195
- raise "omg"
196
- end
199
+ File.write(application, application_contents + <<-CODE)
200
+ class Foo
201
+ def self.omg
202
+ raise "omg"
197
203
  end
198
- CODE
199
- File.write(@test, @test_contents.sub("get :index", "Foo.omg"))
204
+ end
205
+ CODE
206
+ File.write(@test, @test_contents.sub("get :index", "Foo.omg"))
200
207
 
201
- await_reload
208
+ await_reload
202
209
 
203
- assert_speedup do
204
- 2.times { assert_failure spring_test_command, stdout: "RuntimeError: omg" }
205
- end
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(application, application_contents)
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} r 'puts 2'", stdout: "!callback!\n2"
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
@@ -2,3 +2,8 @@ source 'https://rubygems.org'
2
2
 
3
3
  gem 'rails', '~> 3.2.0'
4
4
  gem 'sqlite3'
5
+
6
+ # gem 'listen'
7
+ gem 'rb-inotify', :require => false # linux
8
+ gem 'rb-fsevent', :require => false # mac os x
9
+ gem 'rb-kqueue', :require => false # bsd
@@ -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
- { 'command' => RandomSpringCommand }
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
- @application_commands ||= begin
26
- command = RandomApplicationCommand.new
27
- { 'random' => command, 'r' => command }
28
- end
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 Random Spring Command
48
+ command Random Spring Command
42
49
 
43
50
  Commands for your application:
44
51
 
45
- random, r Random Application Command
52
+ rails omg
53
+ random Random Application Command
46
54
  EOF
47
55
 
48
56
  assert_equal expected_output.chomp, @help.formatted_help