spring 1.0.0 → 1.3.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +6 -1
  4. data/CHANGELOG.md +81 -0
  5. data/CONTRIBUTING.md +52 -0
  6. data/Gemfile +0 -2
  7. data/README.md +137 -54
  8. data/Rakefile +1 -1
  9. data/bin/spring +17 -0
  10. data/lib/spring/application/boot.rb +18 -0
  11. data/lib/spring/application.rb +183 -76
  12. data/lib/spring/application_manager.rb +54 -40
  13. data/lib/spring/binstub.rb +13 -0
  14. data/lib/spring/boot.rb +8 -0
  15. data/lib/spring/client/binstub.rb +150 -39
  16. data/lib/spring/client/help.rb +4 -12
  17. data/lib/spring/client/rails.rb +2 -3
  18. data/lib/spring/client/run.rb +69 -11
  19. data/lib/spring/client/status.rb +2 -2
  20. data/lib/spring/client/stop.rb +6 -21
  21. data/lib/spring/client/version.rb +11 -0
  22. data/lib/spring/client.rb +8 -5
  23. data/lib/spring/command_wrapper.rb +82 -0
  24. data/lib/spring/commands/rails.rb +44 -13
  25. data/lib/spring/commands/rake.rb +0 -4
  26. data/lib/spring/commands.rb +14 -4
  27. data/lib/spring/configuration.rb +11 -2
  28. data/lib/spring/env.rb +36 -10
  29. data/lib/spring/server.rb +9 -35
  30. data/lib/spring/sid.rb +1 -1
  31. data/lib/spring/test/acceptance_test.rb +346 -0
  32. data/lib/spring/test/application.rb +217 -0
  33. data/lib/spring/test/application_generator.rb +121 -0
  34. data/lib/spring/test/rails_version.rb +40 -0
  35. data/lib/spring/test/watcher_test.rb +167 -0
  36. data/lib/spring/test.rb +18 -0
  37. data/lib/spring/version.rb +1 -1
  38. data/lib/spring/watcher/abstract.rb +7 -8
  39. data/lib/spring/watcher/polling.rb +2 -0
  40. data/lib/spring/watcher.rb +4 -8
  41. data/spring.gemspec +2 -2
  42. data/test/acceptance_test.rb +4 -0
  43. data/test/helper.rb +2 -2
  44. data/test/unit/client/version_test.rb +14 -0
  45. data/test/unit/commands_test.rb +23 -1
  46. data/test/unit/watcher_test.rb +2 -184
  47. metadata +30 -17
  48. data/lib/spring/watcher/listen.rb +0 -52
  49. data/test/acceptance/app_test.rb +0 -511
@@ -0,0 +1,217 @@
1
+ require "spring/env"
2
+
3
+ module Spring
4
+ module Test
5
+ class Application
6
+ DEFAULT_TIMEOUT = ENV['CI'] ? 30 : 10
7
+
8
+ attr_reader :root, :spring_env
9
+
10
+ def initialize(root)
11
+ @root = Pathname.new(root)
12
+ @spring_env = Spring::Env.new(root)
13
+ end
14
+
15
+ def exists?
16
+ root.exist?
17
+ end
18
+
19
+ def stdout
20
+ @stdout ||= IO.pipe
21
+ end
22
+
23
+ def stderr
24
+ @stderr ||= IO.pipe
25
+ end
26
+
27
+ def log_file
28
+ @log_file ||= path("tmp/spring.log").open("w+")
29
+ end
30
+
31
+ def env
32
+ @env ||= {
33
+ "GEM_HOME" => gem_home.to_s,
34
+ "GEM_PATH" => gem_home.to_s,
35
+ "HOME" => user_home.to_s,
36
+ "RAILS_ENV" => nil,
37
+ "RACK_ENV" => nil,
38
+ "SPRING_LOG" => log_file.path
39
+ }
40
+ end
41
+
42
+ def path(addition)
43
+ root.join addition
44
+ end
45
+
46
+ def gemfile
47
+ path "Gemfile"
48
+ end
49
+
50
+ def gem_home
51
+ path "vendor/gems/#{RUBY_VERSION}"
52
+ end
53
+
54
+ def user_home
55
+ path "user_home"
56
+ end
57
+
58
+ def spring
59
+ gem_home.join "bin/spring"
60
+ end
61
+
62
+ def rails_version
63
+ @rails_version ||= RailsVersion.new(gemfile.read.match(/gem 'rails', '(.*)'/)[1])
64
+ end
65
+
66
+ def spring_test_command
67
+ "#{rails_version.test_command} #{test}"
68
+ end
69
+
70
+ def stop_spring
71
+ run "#{spring} stop"
72
+ rescue Errno::ENOENT
73
+ end
74
+
75
+ def test
76
+ path "test/#{rails_version.controller_tests_dir}/posts_controller_test.rb"
77
+ end
78
+
79
+ def controller
80
+ path "app/controllers/posts_controller.rb"
81
+ end
82
+
83
+ def application_config
84
+ path "config/application.rb"
85
+ end
86
+
87
+ def spring_config
88
+ path "config/spring.rb"
89
+ end
90
+
91
+ def run(command, opts = {})
92
+ start_time = Time.now
93
+
94
+ Bundler.with_clean_env do
95
+ Process.spawn(
96
+ env,
97
+ command.to_s,
98
+ out: stdout.last,
99
+ err: stderr.last,
100
+ in: :close,
101
+ chdir: root.to_s,
102
+ )
103
+ end
104
+
105
+ _, status = Timeout.timeout(opts.fetch(:timeout, DEFAULT_TIMEOUT)) { Process.wait2 }
106
+
107
+ if pid = spring_env.pid
108
+ @server_pid = pid
109
+ lines = `ps -A -o ppid= -o pid= | egrep '^\\s*#{@server_pid}'`.lines
110
+ @application_pids = lines.map { |l| l.split.last.to_i }
111
+ end
112
+
113
+ output = read_streams
114
+ puts dump_streams(command, output) if ENV["SPRING_DEBUG"]
115
+
116
+ @times << (Time.now - start_time) if @times
117
+
118
+ output.merge(status: status, command: command)
119
+ rescue Timeout::Error => e
120
+ raise e, "Output:\n\n#{dump_streams(command, read_streams)}"
121
+ end
122
+
123
+ def with_timing
124
+ @times = []
125
+ yield
126
+ ensure
127
+ @times = nil
128
+ end
129
+
130
+ def last_time
131
+ @times.last
132
+ end
133
+
134
+ def first_time
135
+ @times.first
136
+ end
137
+
138
+ def timing_ratio
139
+ last_time / first_time
140
+ end
141
+
142
+ def read_streams
143
+ {
144
+ stdout: read_stream(stdout.first),
145
+ stderr: read_stream(stderr.first),
146
+ log: read_stream(log_file)
147
+ }
148
+ end
149
+
150
+ def read_stream(stream)
151
+ output = ""
152
+ while IO.select([stream], [], [], 0.5) && !stream.eof?
153
+ output << stream.readpartial(10240)
154
+ end
155
+ output
156
+ end
157
+
158
+ def dump_streams(command, streams)
159
+ output = "$ #{command}\n"
160
+
161
+ streams.each do |name, stream|
162
+ unless stream.chomp.empty?
163
+ output << "--- #{name} ---\n"
164
+ output << "#{stream.chomp}\n"
165
+ end
166
+ end
167
+
168
+ output << "\n"
169
+ output
170
+ end
171
+
172
+ def debug(artifacts)
173
+ artifacts = artifacts.dup
174
+ artifacts.delete :status
175
+ dump_streams(artifacts.delete(:command), artifacts)
176
+ end
177
+
178
+ def await_reload
179
+ raise "no pid" if @application_pids.nil? || @application_pids.empty?
180
+
181
+ Timeout.timeout(DEFAULT_TIMEOUT) do
182
+ sleep 0.1 while @application_pids.any? { |p| process_alive?(p) }
183
+ end
184
+ end
185
+
186
+ def run!(command, options = {})
187
+ attempts = (options.delete(:retry) || 0) + 1
188
+ artifacts = nil
189
+
190
+ until attempts == 0 || artifacts && artifacts[:status].success?
191
+ artifacts = run(command, options)
192
+ attempts -= 1
193
+ end
194
+
195
+ if artifacts[:status].success?
196
+ artifacts
197
+ else
198
+ raise "command failed\n\n#{debug(artifacts)}"
199
+ end
200
+ end
201
+
202
+ def bundle
203
+ run! "(gem list bundler | grep bundler) || gem install bundler", timeout: nil, retry: 2
204
+ run! "bundle check || bundle update --retry=2", timeout: nil
205
+ end
206
+
207
+ private
208
+
209
+ def process_alive?(pid)
210
+ Process.kill 0, pid
211
+ true
212
+ rescue Errno::ESRCH
213
+ false
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,121 @@
1
+ module Spring
2
+ module Test
3
+ class ApplicationGenerator
4
+ attr_reader :version_constraint, :version, :application
5
+
6
+ def initialize(version_constraint)
7
+ @version_constraint = version_constraint
8
+ @version = RailsVersion.new(version_constraint.split(' ').last)
9
+ @application = Application.new(root)
10
+ @bundled = false
11
+ end
12
+
13
+ def test_root
14
+ Pathname.new Spring::Test.root
15
+ end
16
+
17
+ def root
18
+ test_root.join("apps/rails-#{version.major}-#{version.minor}-spring-#{Spring::VERSION}")
19
+ end
20
+
21
+ def system(command)
22
+ if ENV["SPRING_DEBUG"]
23
+ puts "$ #{command}\n"
24
+ else
25
+ command = "(#{command}) > /dev/null"
26
+ end
27
+
28
+ Kernel.system(command) or raise "command failed: #{command}"
29
+ puts if ENV["SPRING_DEBUG"]
30
+ end
31
+
32
+ def generate
33
+ Bundler.with_clean_env { generate_files }
34
+ install_spring
35
+ generate_scaffold
36
+ end
37
+
38
+ # Sporadic SSL errors keep causing test failures so there are anti-SSL workarounds here
39
+ def generate_files
40
+ system("gem list '^rails$' --installed --version '#{version_constraint}' || " \
41
+ "gem install rails --clear-sources --source http://rubygems.org --version '#{version_constraint}'")
42
+
43
+ @version = RailsVersion.new(`ruby -e 'puts Gem::Specification.find_by_name("rails", "#{version_constraint}").version'`.chomp)
44
+
45
+ skips = %w(--skip-bundle --skip-javascript --skip-sprockets)
46
+ skips << "--skip-spring" if version.bundles_spring?
47
+
48
+ system("rails _#{version}_ new #{application.root} #{skips.join(' ')}")
49
+ raise "application generation failed" unless application.exists?
50
+
51
+ FileUtils.mkdir_p(application.gem_home)
52
+ FileUtils.mkdir_p(application.user_home)
53
+ FileUtils.rm_rf(application.path("test/performance"))
54
+
55
+ File.write(application.gemfile, "#{application.gemfile.read}gem 'spring', '#{Spring::VERSION}'\n")
56
+
57
+ if version.needs_testunit?
58
+ File.write(application.gemfile, "#{application.gemfile.read}gem 'spring-commands-testunit'\n")
59
+ end
60
+
61
+ File.write(application.gemfile, application.gemfile.read.sub("https://rubygems.org", "http://rubygems.org"))
62
+
63
+ if application.path("bin").exist?
64
+ FileUtils.cp_r(application.path("bin"), application.path("bin_original"))
65
+ end
66
+ end
67
+
68
+ def generate_if_missing
69
+ generate unless application.exists?
70
+ end
71
+
72
+ def install_spring
73
+ return if @installed
74
+
75
+ build_and_install_gems
76
+
77
+ application.bundle
78
+
79
+ FileUtils.rm_rf application.path("bin")
80
+
81
+ if application.path("bin_original").exist?
82
+ FileUtils.cp_r application.path("bin_original"), application.path("bin")
83
+ end
84
+
85
+ application.run! "#{application.spring} binstub --all"
86
+ @installed = true
87
+ end
88
+
89
+ def manually_built_gems
90
+ %w(spring)
91
+ end
92
+
93
+ def build_and_install_gems
94
+ manually_built_gems.each do |name|
95
+ spec = Gem::Specification.find_by_name(name)
96
+
97
+ FileUtils.cd(spec.gem_dir) do
98
+ FileUtils.rm(Dir.glob("#{name}-*.gem"))
99
+ system("gem build #{name}.gemspec 2>&1")
100
+ end
101
+
102
+ application.run! "gem install #{spec.gem_dir}/#{name}-*.gem", timeout: nil
103
+ end
104
+ end
105
+
106
+ def copy_to(path)
107
+ system("rm -rf #{path}")
108
+ system("cp -r #{application.root} #{path}")
109
+ end
110
+
111
+ def generate_scaffold
112
+ application.run! "bundle exec rails g scaffold post title:string"
113
+ application.run! "bundle exec rake db:migrate db:test:clone"
114
+ end
115
+
116
+ def gemspec(name)
117
+ "#{Gem::Specification.find_by_name(name).gem_dir}/#{name}.gemspec"
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,40 @@
1
+ module Spring
2
+ module Test
3
+ class RailsVersion
4
+ attr_reader :version
5
+
6
+ def initialize(string)
7
+ @version = Gem::Version.new(string)
8
+ end
9
+
10
+ def rails_3?
11
+ version < Gem::Version.new("4.0.0")
12
+ end
13
+ alias needs_testunit? rails_3?
14
+
15
+ def test_command
16
+ needs_testunit? ? 'bin/testunit' : 'bin/rake test'
17
+ end
18
+
19
+ def controller_tests_dir
20
+ rails_3? ? 'functional' : 'controllers'
21
+ end
22
+
23
+ def bundles_spring?
24
+ version.segments.take(2) == [4, 1] || version > Gem::Version.new("4.1")
25
+ end
26
+
27
+ def major
28
+ version.segments[0]
29
+ end
30
+
31
+ def minor
32
+ version.segments[1]
33
+ end
34
+
35
+ def to_s
36
+ version.to_s
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,167 @@
1
+ require "tmpdir"
2
+ require "fileutils"
3
+ require "timeout"
4
+ require "active_support/core_ext/numeric/time"
5
+
6
+ module Spring
7
+ module Test
8
+ class WatcherTest < ActiveSupport::TestCase
9
+ runnables.delete self # prevent Minitest running this class
10
+
11
+ LATENCY = 0.001
12
+ TIMEOUT = 1
13
+
14
+ attr_accessor :dir
15
+
16
+ def watcher_class
17
+ raise NotImplementedError
18
+ end
19
+
20
+ def watcher
21
+ @watcher ||= watcher_class.new(dir, LATENCY)
22
+ end
23
+
24
+ def setup
25
+ @dir = File.realpath(Dir.mktmpdir)
26
+ end
27
+
28
+ def teardown
29
+ FileUtils.remove_entry_secure @dir
30
+ watcher.stop
31
+ end
32
+
33
+ def touch(file, mtime = nil)
34
+ options = {}
35
+ options[:mtime] = mtime if mtime
36
+ FileUtils.touch(file, options)
37
+ end
38
+
39
+ def assert_stale
40
+ timeout = Time.now + TIMEOUT
41
+ sleep LATENCY until watcher.stale? || Time.now > timeout
42
+ assert watcher.stale?
43
+ end
44
+
45
+ def assert_not_stale
46
+ sleep LATENCY * 10
47
+ assert !watcher.stale?
48
+ end
49
+
50
+ test "starting with no file" do
51
+ file = "#{@dir}/omg"
52
+ touch file, Time.now - 2.seconds
53
+
54
+ watcher.start
55
+ watcher.add file
56
+
57
+ assert_not_stale
58
+ touch file, Time.now
59
+ assert_stale
60
+ end
61
+
62
+ test "is stale when a watched file is updated" do
63
+ file = "#{@dir}/omg"
64
+ touch file, Time.now - 2.seconds
65
+
66
+ watcher.add file
67
+ watcher.start
68
+
69
+ assert_not_stale
70
+ touch file, Time.now
71
+ assert_stale
72
+ end
73
+
74
+ test "is stale when removing files" do
75
+ file = "#{@dir}/omg"
76
+ touch file, Time.now
77
+
78
+ watcher.add file
79
+ watcher.start
80
+
81
+ assert_not_stale
82
+ FileUtils.rm(file)
83
+ assert_stale
84
+ end
85
+
86
+ test "is stale when files are added to a watched directory" do
87
+ subdir = "#{@dir}/subdir"
88
+ FileUtils.mkdir(subdir)
89
+
90
+ watcher.add subdir
91
+ watcher.start
92
+
93
+ assert_not_stale
94
+ touch "#{subdir}/foo", Time.now - 1.minute
95
+ assert_stale
96
+ end
97
+
98
+ test "is stale when a file is changed in a watched directory" do
99
+ subdir = "#{@dir}/subdir"
100
+ FileUtils.mkdir(subdir)
101
+ touch "#{subdir}/foo", Time.now - 1.minute
102
+
103
+ watcher.add subdir
104
+ watcher.start
105
+
106
+ assert_not_stale
107
+ touch "#{subdir}/foo", Time.now
108
+ assert_stale
109
+ end
110
+
111
+ test "adding doesn't wipe stale state" do
112
+ file = "#{@dir}/omg"
113
+ file2 = "#{@dir}/foo"
114
+ touch file, Time.now - 2.seconds
115
+ touch file2, Time.now - 2.seconds
116
+
117
+ watcher.add file
118
+ watcher.start
119
+
120
+ assert_not_stale
121
+
122
+ touch file, Time.now
123
+ watcher.add file2
124
+
125
+ assert_stale
126
+ end
127
+
128
+ test "on stale" do
129
+ file = "#{@dir}/omg"
130
+ touch file, Time.now - 2.seconds
131
+
132
+ stale = false
133
+ watcher.on_stale { stale = true }
134
+
135
+ watcher.add file
136
+ watcher.start
137
+
138
+ touch file, Time.now
139
+
140
+ Timeout.timeout(1) { sleep 0.01 until stale }
141
+ assert stale
142
+
143
+ # Check that we only get notified once
144
+ stale = false
145
+ sleep LATENCY * 3
146
+ assert !stale
147
+ end
148
+
149
+ test "add relative path" do
150
+ File.write("#{dir}/foo", "foo")
151
+ watcher.add "foo"
152
+ assert_equal ["#{dir}/foo"], watcher.files.to_a
153
+ end
154
+
155
+ test "add dot relative path" do
156
+ File.write("#{dir}/foo", "foo")
157
+ watcher.add "./foo"
158
+ assert_equal ["#{dir}/foo"], watcher.files.to_a
159
+ end
160
+
161
+ test "add non existent file" do
162
+ watcher.add './foobar'
163
+ assert watcher.files.empty?
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,18 @@
1
+ require "active_support"
2
+ require "active_support/test_case"
3
+
4
+ ActiveSupport.test_order = :random
5
+
6
+ module Spring
7
+ module Test
8
+ class << self
9
+ attr_accessor :root
10
+ end
11
+
12
+ require "spring/test/application"
13
+ require "spring/test/application_generator"
14
+ require "spring/test/rails_version"
15
+ require "spring/test/watcher_test"
16
+ require "spring/test/acceptance_test"
17
+ end
18
+ end
@@ -1,3 +1,3 @@
1
1
  module Spring
2
- VERSION = "1.0.0"
2
+ VERSION = "1.3.0"
3
3
  end
@@ -22,7 +22,7 @@ module Spring
22
22
  @files = Set.new
23
23
  @directories = Set.new
24
24
  @stale = false
25
- @io_listener = nil
25
+ @listeners = []
26
26
  end
27
27
 
28
28
  def add(*items)
@@ -55,15 +55,14 @@ module Spring
55
55
  @stale
56
56
  end
57
57
 
58
- def mark_stale
59
- @stale = true
60
- @io_listener.write "." if @io_listener
58
+ def on_stale(&block)
59
+ @listeners << block
61
60
  end
62
61
 
63
- def to_io
64
- read, write = IO.pipe
65
- @io_listener = write
66
- read
62
+ def mark_stale
63
+ return if stale?
64
+ @stale = true
65
+ @listeners.each(&:call)
67
66
  end
68
67
 
69
68
  def restart
@@ -1,3 +1,5 @@
1
+ require "spring/watcher"
2
+
1
3
  module Spring
2
4
  module Watcher
3
5
  class Polling < Abstract
@@ -9,15 +9,11 @@ module Spring
9
9
  end
10
10
 
11
11
  def self.watch_method=(method)
12
- case method
13
- when :polling
14
- require_relative "watcher/polling"
15
- @watch_method = Watcher::Polling
16
- when :listen
17
- require_relative "watcher/listen"
18
- @watch_method = Watcher::Listen
19
- else
12
+ if method.is_a?(Class)
20
13
  @watch_method = method
14
+ else
15
+ require "spring/watcher/#{method}"
16
+ @watch_method = Watcher.const_get(method.to_s.gsub(/(^.|_.)/) { $1[-1].upcase })
21
17
  end
22
18
  end
23
19
 
data/spring.gemspec CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |gem|
10
10
  gem.email = ["j@jonathanleighton.com"]
11
11
  gem.description = %q{Rails application preloader}
12
12
  gem.summary = %q{Rails application preloader}
13
- gem.homepage = "http://github.com/jonleighton/spring"
13
+ gem.homepage = "http://github.com/rails/spring"
14
14
  gem.license = "MIT"
15
15
 
16
16
  gem.files = `git ls-files`.split($/)
@@ -18,6 +18,6 @@ Gem::Specification.new do |gem|
18
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
19
  gem.require_paths = ["lib"]
20
20
 
21
- gem.add_development_dependency 'activesupport'
21
+ gem.add_development_dependency 'activesupport', '~> 4.2.0'
22
22
  gem.add_development_dependency 'rake'
23
23
  end
@@ -0,0 +1,4 @@
1
+ require "helper"
2
+
3
+ class AcceptanceTest < Spring::Test::AcceptanceTest
4
+ end
data/test/helper.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
2
2
 
3
3
  require "bundler/setup"
4
- require "active_support/test_case"
5
4
  require "minitest/autorun"
6
5
 
7
- TEST_ROOT = File.expand_path('..', __FILE__)
6
+ require "spring/test"
7
+ Spring::Test.root = File.expand_path('..', __FILE__)
@@ -0,0 +1,14 @@
1
+ require 'helper'
2
+ require 'spring/client'
3
+
4
+ class VersionTest < ActiveSupport::TestCase
5
+ test "outputs current version number" do
6
+ version = Spring::Client::Version.new 'version'
7
+
8
+ out, err = capture_io do
9
+ version.call
10
+ end
11
+
12
+ assert_equal "Spring version #{Spring::VERSION}", out.chomp
13
+ end
14
+ end