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.
@@ -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