rack-unreloader 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7665e2203b2b2f7ac74d05e6c0b5b2c60d7d89d8
4
+ data.tar.gz: 19cd1b3fcf725bdaf58db40ba67426c38b3823d1
5
+ SHA512:
6
+ metadata.gz: 7810e9036260c1a517f544783e755997deeb759fcb56e05fdc2ee16109de4c74c907f94a019fe42c503610255797d4f04f76060ff8dde732037eb15df52b90a2
7
+ data.tar.gz: 17a0ea4463d61cfb959e72ce724c28fecba47dc7ac098ab0b01559c17f8e90026436b982da342335610d281f4fd652b3f13bfd5b1d1b10555d8ec590410036df
data/CHANGELOG ADDED
@@ -0,0 +1,3 @@
1
+ = 0.9.0 (2014-08-04)
2
+
3
+ * Initial public release
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2014 Jeremy Evans
2
+ Copyright (c) 2011 Padrino
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,177 @@
1
+ = Rack::Unreloader
2
+
3
+ Rack::Unreloader is a rack library that reloads application files when it
4
+ detects changes, unloading constants defined in those files before reloading.
5
+ Like other rack libraries for reloading, this can make application development
6
+ much faster, as you don't need to restart the whole application when you change
7
+ a single file. Unlike most other rack libraries for reloading, this unloads
8
+ constants before requiring files, avoiding issues when loading a file is not
9
+ idempotent.
10
+
11
+ == Installation
12
+
13
+ gem install rack-unreloader
14
+
15
+ == Source Code
16
+
17
+ Source code is available on GitHub at https://github.com/jeremyevans/rack-unreloader
18
+
19
+ == Basic Usage
20
+
21
+ Assuming a basic web application stored in +app.rb+:
22
+
23
+ require 'roda'
24
+
25
+ class App < Roda
26
+ route do |r|
27
+ "Hello world!"
28
+ end
29
+ end
30
+
31
+ With a basic +config.ru+ like this:
32
+
33
+ require './app.rb'
34
+ run App
35
+
36
+ Change +config.ru+ to:
37
+
38
+ require 'rack-unreloader'
39
+ Unreloader = Rack::Unreloader.new{App}
40
+ require 'roda'
41
+ Unreloader.require './app.rb'
42
+ run Unreloader
43
+
44
+ The block you pass to Rack::Unreloader.new should return the rack application
45
+ to use. If you make any changes to +app.rb+, <tt>Rack::Unreloader</tt> will remove any
46
+ constants defined by requiring +app.rb+, and rerequire the file.
47
+
48
+ Note that this causes problems if +app.rb+ loads any new libraries that define
49
+ constants, as it will unload those constants first. This is why the example
50
+ code requires the +roda+ library normally before requiring +app.rb+ using
51
+ <tt>Rack::Unreloader</tt>.
52
+
53
+ However, if +app.rb+ requires more than a single file, it is more
54
+ practical to tell <tt>Rack::Unreloader</tt> to only unload specific subclasses:
55
+
56
+ require 'rack-unreloader'
57
+ Unreloader = Rack::Unreloader.new(:subclasses=>%w'Roda'){App}
58
+ Unreloader.require './app.rb'
59
+ run Unreloader
60
+
61
+ When the +:subclasses+ option is given, only subclasses of the given classes
62
+ will be unloaded before reloading the file. It is recommended that
63
+ you use a +:subclasses+ option when using <tt>Rack::Unreloader</tt>.
64
+
65
+ == Dependency Handling
66
+
67
+ If your +app.rb+ requires a +models.rb+ file that you also want to get reloaded:
68
+
69
+ require 'roda'
70
+ require './models.rb'
71
+
72
+ class App < Roda
73
+ route do |r|
74
+ "Hello world!"
75
+ end
76
+ end
77
+
78
+ You can change +app.rb+ from using:
79
+
80
+ require './models.rb'
81
+
82
+ to using:
83
+
84
+ Unreloader.require './models.rb'
85
+
86
+ The reason that the <tt>Rack::Unreloader</tt> instance is assigned to a constant in
87
+ +config.ru+ is to make it easy to add reloadable dependencies in this way.
88
+
89
+ It's even a better idea to require this dependency manually in +config.ru+,
90
+ before requiring +app.rb+:
91
+
92
+ require 'rack-unreloader'
93
+ Unreloader = Rack::Unreloader.new(:subclasses=>%w'Roda Sequel::Model'){App}
94
+ Unreloader.require './models.rb'
95
+ Unreloader.require './app.rb'
96
+ run Unreloader
97
+
98
+ This way, changing your +app.rb+ file will not reload your +models.rb+ file.
99
+
100
+ == Only in Development Mode
101
+
102
+ In general, you are only going to want to run this in development mode.
103
+ Assuming you use +RACK_ENV+ to determine development mode, you can change
104
+ +config.ru+ to:
105
+
106
+ if ENV['RACK_ENV'] == 'development'
107
+ require 'rack-unreloader'
108
+ Unreloader = Rack::Unreloader.new{App}
109
+ Unreloader.require './models.rb'
110
+ Unreloader.require './app.rb'
111
+ run Unreloader
112
+ else
113
+ require './app.rb'
114
+ run App
115
+ end
116
+
117
+ If there are dependencies that you don't want to require directly in your
118
+ +config.ru+, but you do want to use <tt>Rack::Unreloader</tt> for them in
119
+ development, you can do:
120
+
121
+ (defined?(Unreloader) ? Unreloader : Kernel).require './models.rb'
122
+
123
+ == Modules
124
+
125
+ This reloader also handles modules. Since modules do not have superclasses,
126
+ if you are using the +:subclasses+ option to specify specific subclasses, you
127
+ need to specify the module name if you want to reload it:
128
+
129
+ Unreloader = Rack::Unreloader.new(:subclasses=>%w'MyModule'){App}
130
+
131
+ == Requiring
132
+
133
+ Rack::Unreloader#require is a little different than require in that it takes
134
+ a file glob, not a normal require path. For that reason, you must specify
135
+ the extension when requiring the file, and it will only look in the current
136
+ directory by default:
137
+
138
+ Unreloader.require 'app.rb'
139
+
140
+ If you want to require a file in a different directory, you need to provide the
141
+ full path:
142
+
143
+ Unreloader.require '/path/to/app.rb'
144
+
145
+ You can use the usual file globbing:
146
+
147
+ Unreloader.require 'models/*.rb'
148
+
149
+ == History
150
+
151
+ Rack::Unreloader was derived from Padrino's reloader. It is significantly smaller
152
+ as it cuts out a lot of Padrino-specific code, and it forces the user to manually
153
+ specify what files to monitor.
154
+
155
+ == Caveats
156
+
157
+ Unloading constants and reloading files has a ton of corner cases that this
158
+ will not handle correctly. If it isn't doing what you expect, add a logger:
159
+
160
+ Rack::Unreloader.new(:logger=>Logger.new($stdout)){App}
161
+
162
+ Unloading constants causes issues whenever references to the constant are
163
+ cached anywhere instead of looking up the constant by name. This is fairly
164
+ common, and using this library can cause a memory leak in such a case.
165
+
166
+ Approaches that load a fresh environment for every request (or a fresh
167
+ environment anytime there are any changes) are going to be more robust than
168
+ this approach, but probably slower. Be aware that you are trading robustness
169
+ for speed when using this library.
170
+
171
+ == License
172
+
173
+ MIT
174
+
175
+ == Maintainer
176
+
177
+ Jeremy Evans <code@jeremyevans.net>
data/Rakefile ADDED
@@ -0,0 +1,68 @@
1
+ require "rake"
2
+ require "rake/clean"
3
+
4
+ CLEAN.include ["rack-unreloader-*.gem", "rdoc"]
5
+
6
+ desc "Build rack-unreloader gem"
7
+ task :package=>[:clean] do |p|
8
+ sh %{#{FileUtils::RUBY} -S gem build rack-unreloader.gemspec}
9
+ end
10
+
11
+ ### Specs
12
+
13
+ begin
14
+ begin
15
+ # RSpec 1
16
+ require "spec/rake/spectask"
17
+ spec_class = Spec::Rake::SpecTask
18
+ spec_files_meth = :spec_files=
19
+ rescue LoadError
20
+ # RSpec 2
21
+ require "rspec/core/rake_task"
22
+ spec_class = RSpec::Core::RakeTask
23
+ spec_files_meth = :pattern=
24
+ end
25
+
26
+ spec = lambda do |name, files, d|
27
+ lib_dir = File.join(File.dirname(File.expand_path(__FILE__)), 'lib')
28
+ ENV['RUBYLIB'] ? (ENV['RUBYLIB'] += ":#{lib_dir}") : (ENV['RUBYLIB'] = lib_dir)
29
+ desc d
30
+ spec_class.new(name) do |t|
31
+ t.send(spec_files_meth, files)
32
+ end
33
+ end
34
+
35
+ task :default => [:spec]
36
+ spec.call("spec", Dir["spec/*_spec.rb"], "Run specs")
37
+ rescue LoadError
38
+ task :default do
39
+ puts "Must install rspec to run the default task (which runs specs)"
40
+ end
41
+ end
42
+
43
+ ### RDoc
44
+
45
+ RDOC_DEFAULT_OPTS = ["--quiet", "--line-numbers", "--inline-source", '--title', 'Rack::Unreloader: Reload application when files change, unloading constants first']
46
+
47
+ begin
48
+ gem 'hanna-nouveau'
49
+ RDOC_DEFAULT_OPTS.concat(['-f', 'hanna'])
50
+ rescue Gem::LoadError
51
+ end
52
+
53
+ rdoc_task_class = begin
54
+ require "rdoc/task"
55
+ RDoc::Task
56
+ rescue LoadError
57
+ require "rake/rdoctask"
58
+ Rake::RDocTask
59
+ end
60
+
61
+ RDOC_OPTS = RDOC_DEFAULT_OPTS + ['--main', 'README.rdoc']
62
+
63
+ rdoc_task_class.new do |rdoc|
64
+ rdoc.rdoc_dir = "rdoc"
65
+ rdoc.options += RDOC_OPTS
66
+ rdoc.rdoc_files.add %w"README.rdoc CHANGELOG MIT-LICENSE lib/**/*.rb"
67
+ end
68
+
@@ -0,0 +1,264 @@
1
+ require 'set'
2
+
3
+ module Rack
4
+ # Reloading application that unloads constants before reloading the relevant
5
+ # files, calling the new rack app if it gets reloaded.
6
+ class Unreloader
7
+ class Reloader
8
+ # Reference to ::File as File would return Rack::File by default.
9
+ F = ::File
10
+
11
+ # Regexp for valid constant names, to prevent code execution.
12
+ VALID_CONSTANT_NAME_REGEXP = /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/.freeze
13
+
14
+ # Setup the reloader. Supports :logger and :subclasses options, see
15
+ # Rack::Unloader.new for details.
16
+ def initialize(opts={})
17
+ @logger = opts[:logger]
18
+ @classes = opts[:subclasses] ? Array(opts[:subclasses]).map{|s| s.to_s} : %w'Object'
19
+
20
+ # Hash of files being monitored for changes, keyed by absolute path of file name,
21
+ # with values being the last modified time (or nil if the file has not yet been loaded).
22
+ @monitor_files = {}
23
+
24
+ # Hash keyed by absolute path of file name, storing constants and other
25
+ # filenames that the key loads. Values should be hashes with :constants
26
+ # and :features keys, and arrays of values.
27
+ @files = {}
28
+
29
+ # Similar to @files, but stores previous entries, used when rolling back.
30
+ @old_entries = {}
31
+ end
32
+
33
+ # Tries to find a declared constant with the name specified
34
+ # in the string. It raises a NameError when the name is not in CamelCase
35
+ # or is not initialized.
36
+ def constantize(s)
37
+ s = s.to_s
38
+ if m = VALID_CONSTANT_NAME_REGEXP.match(s)
39
+ Object.module_eval("::#{m[1]}", __FILE__, __LINE__)
40
+ else
41
+ log("#{s.inspect} is not a valid constant name!")
42
+ end
43
+ end
44
+
45
+ # Log the given string at info level if there is a logger.
46
+ def log(s)
47
+ @logger.info(s) if @logger
48
+ end
49
+
50
+ # If there are any changed files, reload them. If there are no changed
51
+ # files, do nothing.
52
+ def reload!
53
+ @monitor_files.to_a.each do |file, time|
54
+ if file_changed?(file, time)
55
+ safe_load(file)
56
+ end
57
+ end
58
+ end
59
+
60
+ # Require the given dependencies, monitoring them for changes.
61
+ # Paths should be a file glob or an array of file globs.
62
+ def require_dependencies(paths)
63
+ options = {:cyclic => true}
64
+ error = nil
65
+
66
+ Array(paths).
67
+ flatten.
68
+ map{|path| Dir.glob(path).sort_by{|filename| filename.count('/')}}.
69
+ flatten.
70
+ map{|path| F.expand_path(path)}.
71
+ uniq.
72
+ each do |file|
73
+
74
+ @monitor_files[file] = nil
75
+ begin
76
+ safe_load(file, options)
77
+ rescue NameError, LoadError => error
78
+ log "Cyclic dependency reload for #{error}"
79
+ rescue Exception => error
80
+ break
81
+ end
82
+ end
83
+
84
+ if error
85
+ log error
86
+ raise error
87
+ end
88
+ end
89
+
90
+ # Requires the given file, logging which constants or features are added
91
+ # by the require, and rolling back the constants and features if there
92
+ # are any errors.
93
+ def safe_load(file, options={})
94
+ return unless @monitor_files.has_key?(file)
95
+ return unless options[:force] || file_changed?(file)
96
+
97
+ prepare(file) # might call #safe_load recursively
98
+ log "#{@monitor_files[file] ? 'Reloading' : 'Loading'} #{file}"
99
+ begin
100
+ require(file)
101
+ commit(file)
102
+ rescue Exception
103
+ if !options[:cyclic]
104
+ log "Failed to load #{file}; removing partially defined constants"
105
+ end
106
+ rollback(file)
107
+ raise
108
+ end
109
+ end
110
+
111
+ # Removes the specified constant.
112
+ def remove_constant(const)
113
+ base, _, object = const.to_s.rpartition('::')
114
+ base = base.empty? ? Object : constantize(base)
115
+ base.send :remove_const, object
116
+ log "Removed constant #{const}"
117
+ rescue NameError
118
+ end
119
+
120
+ # Remove a feature if it is being monitored for reloading, so it
121
+ # can be required again.
122
+ def remove_feature(file)
123
+ $LOADED_FEATURES.delete(file) if @monitor_files.has_key?(file)
124
+ end
125
+
126
+ # Unload all reloadable constants and features, and clear the list
127
+ # of files to monitor.
128
+ def clear!
129
+ @files.keys.each do |file|
130
+ remove(file)
131
+ remove_feature(file)
132
+ end
133
+ @monitor_files = {}
134
+ @old_entries = {}
135
+ end
136
+
137
+ # Remove the given file, removing any constants and other files loaded
138
+ # by the file.
139
+ def remove(name)
140
+ file = @files[name] || return
141
+ file[:constants].each{|constant| remove_constant(constant)}
142
+ file[:features].each{|feature| remove_feature(feature)}
143
+ @files.delete(name)
144
+ end
145
+
146
+ # Store the currently loaded classes and features, so in case of an error
147
+ # this state can be rolled back to.
148
+ def prepare(name)
149
+ file = remove(name)
150
+ old_features = Set.new($LOADED_FEATURES)
151
+ @old_entries[name] = {:constants => all_classes, :features => old_features}
152
+ features = file && file[:features] || []
153
+ features.each{|feature| safe_load(feature, :force => true)}
154
+ remove_feature(name) if old_features.include?(name)
155
+ end
156
+
157
+ # Commit the changed state after requiring the the file, recording the new
158
+ # classes and features added by the file.
159
+ def commit(name)
160
+ entry = {
161
+ :constants => new_classes(@old_entries[name][:constants]),
162
+ :features => Set.new($LOADED_FEATURES) - @old_entries[name][:features] - [name]
163
+ }
164
+ @files[name] = entry
165
+ @old_entries.delete(name)
166
+ @monitor_files[name] = modified_at(name)
167
+ end
168
+
169
+ # Rollback the changes made by requiring the file, restoring the previous state.
170
+ def rollback(name)
171
+ new_classes(@old_entries[name][:constants]).each{|klass| remove_constant(klass)}
172
+ @old_entries.delete(name)
173
+ end
174
+
175
+ private
176
+
177
+ # Return a set of all classes in the ObjectSpace.
178
+ def all_classes
179
+ rs = Set.new
180
+
181
+ ObjectSpace.each_object(Module).each do |klass|
182
+ if monitored_class?(klass)
183
+ rs << klass
184
+ end
185
+ end
186
+
187
+ rs
188
+ end
189
+
190
+ # Return whether the given klass is a monitored class that could
191
+ # be unloaded.
192
+ def monitored_class?(klass)
193
+ @classes.any? do |c|
194
+ c = constantize(c) rescue false
195
+
196
+ if klass.is_a?(Class)
197
+ # Reload the class if it is a subclass if the current class
198
+ (klass < c) rescue false
199
+ elsif c == Object
200
+ # If reloading for all classes, reload for all modules as well
201
+ true
202
+ else
203
+ # Otherwise, reload only if the module matches exactly, since
204
+ # modules don't have superclasses.
205
+ klass == c
206
+ end
207
+ end
208
+ end
209
+
210
+ # Return a set of all classes in the ObjectSpace that are not in the
211
+ # given set of classes.
212
+ def new_classes(snapshot)
213
+ all_classes - snapshot
214
+ end
215
+
216
+ # Returns true if the file is new or it's modification time changed.
217
+ def file_changed?(file, time = @monitor_files[file])
218
+ !time || modified_at(file) > time
219
+ end
220
+
221
+ # Return the time the file was modified at. This can be overridden
222
+ # to base the reloading on something other than the file's modification
223
+ # time.
224
+ def modified_at(file)
225
+ F.mtime(file)
226
+ end
227
+ end
228
+
229
+ # The Rack::Unreloader::Reloader instead related to this instance.
230
+ attr_reader :reloader
231
+
232
+ # Setup the reloader. Options:
233
+ #
234
+ # :cooldown :: The number of seconds to wait between checks for changed files.
235
+ # Defaults to 1.
236
+ # :logger :: A Logger instance which will log information related to reloading.
237
+ # :subclasses :: A string or array of strings of class names that should be unloaded.
238
+ # Any classes that are not subclasses of these classes will not be
239
+ # unloaded. This also handles modules, but module names given must
240
+ # match exactly, since modules don't have superclasses.
241
+ def initialize(opts={}, &block)
242
+ @app_block = block
243
+ @cooldown = opts[:cooldown] || 1
244
+ @last = Time.at(0)
245
+ @reloader = Reloader.new(opts)
246
+ @reloader.reload!
247
+ end
248
+
249
+ # If the cooldown time has been passed, reload any application files that have changed.
250
+ # Call the app with the environment.
251
+ def call(env)
252
+ if @cooldown && Time.now > @last + @cooldown
253
+ Thread.exclusive{@reloader.reload!}
254
+ @last = Time.now
255
+ end
256
+ @app_block.call.call(env)
257
+ end
258
+
259
+ # Add a file glob or array of file globs to monitor for changes.
260
+ def require(depends)
261
+ @reloader.require_dependencies(depends)
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,145 @@
1
+ require File.join(File.dirname(File.expand_path(__FILE__)), '../lib/rack/unreloader')
2
+
3
+ module ModifiedAt
4
+ def set_modified_time(file, time)
5
+ modified_times[File.expand_path(file)] = time
6
+ end
7
+
8
+ def modified_times
9
+ @modified_times ||= {}
10
+ end
11
+
12
+ private
13
+
14
+ def modified_at(file)
15
+ modified_times[file] || super
16
+ end
17
+ end
18
+
19
+ describe Rack::Unreloader do
20
+ def code(i)
21
+ "class App; def self.call(env) @a end; @a ||= []; @a << #{i}; end"
22
+ end
23
+
24
+ def update_app(code, file=@filename)
25
+ ru.reloader.set_modified_time(file, @i += 1)
26
+ File.open(file, 'wb'){|f| f.write(code)}
27
+ end
28
+
29
+ def logger
30
+ return @logger if @logger
31
+ @logger = []
32
+ def @logger.method_missing(meth, log)
33
+ self << log
34
+ end
35
+ @logger
36
+ end
37
+
38
+ def ru(opts={})
39
+ return @ru if @ru
40
+ block = opts[:block] || proc{App}
41
+ @ru = Rack::Unreloader.new({:logger=>logger, :cooldown=>0}.merge(opts), &block)
42
+ @ru.reloader.extend ModifiedAt
43
+ Object.const_set(:RU, @ru)
44
+ update_app(opts[:code]||code(1))
45
+ yield if block_given?
46
+ @ru.require 'spec/app.rb'
47
+ @ru
48
+ end
49
+
50
+ def log_match(*logs)
51
+ logs.length == @logger.length
52
+ logs.zip(@logger).each{|l, log| l.is_a?(String) ? log.should == l : log.should =~ l}
53
+ end
54
+
55
+ before do
56
+ @i = 0
57
+ @filename = 'spec/app.rb'
58
+ end
59
+
60
+ after do
61
+ ru.reloader.clear!
62
+ Object.send(:remove_const, :RU)
63
+ Object.send(:remove_const, :App) if defined?(::App)
64
+ Object.send(:remove_const, :App2) if defined?(::App2)
65
+ Dir['spec/app*.rb'].each{|f| File.delete(f)}
66
+ end
67
+
68
+ it "it should unload constants contained in file and reload file if file changes" do
69
+ ru.call({}).should == [1]
70
+ update_app(code(2))
71
+ ru.call({}).should == [2]
72
+ log_match(%r{\ALoading.*spec/app\.rb\z}, "Removed constant App", %r{\AReloading.*spec/app\.rb\z})
73
+ end
74
+
75
+ it "it should pickup files added as dependencies" do
76
+ ru.call({}).should == [1]
77
+ update_app("RU.require 'spec/app2.rb'; class App; def self.call(env) [@a, App2.call(env)] end; @a ||= []; @a << 2; end")
78
+ update_app("class App2; def self.call(env) @a end; @a ||= []; @a << 3; end", 'spec/app2.rb')
79
+ ru.call({}).should == [[2], [3]]
80
+ update_app("class App2; def self.call(env) @a end; @a ||= []; @a << 4; end", 'spec/app2.rb')
81
+ ru.call({}).should == [[2], [4]]
82
+ update_app("RU.require 'spec/app2.rb'; class App; def self.call(env) [@a, App2.call(env)] end; @a ||= []; @a << 2; end")
83
+ log_match(%r{\ALoading.*spec/app\.rb\z}, "Removed constant App", %r{\AReloading.*spec/app\.rb\z},
84
+ %r{\ALoading.*spec/app2\.rb\z}, "Removed constant App2", %r{\AReloading.*spec/app2\.rb\z})
85
+ end
86
+
87
+ it "it should support :subclasses option and only unload subclasses of given class" do
88
+ ru(:subclasses=>'App').call({}).should == [1]
89
+ update_app("RU.require 'spec/app2.rb'; class App; def self.call(env) [@a, App2.call(env)] end; @a ||= []; @a << 2; end")
90
+ update_app("class App2 < App; def self.call(env) @a end; @a ||= []; @a << 3; end", 'spec/app2.rb')
91
+ ru.call({}).should == [[1, 2], [3]]
92
+ update_app("class App2 < App; def self.call(env) @a end; @a ||= []; @a << 4; end", 'spec/app2.rb')
93
+ ru.call({}).should == [[1, 2], [4]]
94
+ update_app("RU.require 'spec/app2.rb'; class App; def self.call(env) [@a, App2.call(env)] end; @a ||= []; @a << 2; end")
95
+ log_match(%r{\ALoading.*spec/app\.rb\z}, %r{\AReloading.*spec/app\.rb\z},
96
+ %r{\ALoading.*spec/app2\.rb\z}, "Removed constant App2", %r{\AReloading.*spec/app2\.rb\z})
97
+ end
98
+
99
+ it "it log invalid constant names in :subclasses options" do
100
+ ru(:subclasses=>%w'1 Object').call({}).should == [1]
101
+ logger.uniq!
102
+ log_match('"1" is not a valid constant name!', %r{\ALoading.*spec/app\.rb\z})
103
+ end
104
+
105
+ it "it should unload modules before reloading similar to classes" do
106
+ ru(:code=>"module App; def self.call(env) @a end; @a ||= []; @a << 1; end").call({}).should == [1]
107
+ update_app("module App; def self.call(env) @a end; @a ||= []; @a << 2; end")
108
+ ru.call({}).should == [2]
109
+ log_match(%r{\ALoading.*spec/app\.rb\z}, "Removed constant App", %r{\AReloading.*spec/app\.rb\z})
110
+ end
111
+
112
+ it "it should unload specific modules by name via :subclasses option" do
113
+ ru(:subclasses=>'App', :code=>"module App; def self.call(env) @a end; @a ||= []; @a << 1; end").call({}).should == [1]
114
+ update_app("module App; def self.call(env) @a end; @a ||= []; @a << 2; end")
115
+ ru.call({}).should == [2]
116
+ log_match(%r{\ALoading.*spec/app\.rb\z}, "Removed constant App", %r{\AReloading.*spec/app\.rb\z})
117
+ end
118
+
119
+ it "it should not unload modules by name if :subclasses option used and module not present" do
120
+ ru(:subclasses=>'Foo', :code=>"module App; def self.call(env) @a end; @a ||= []; @a << 1; end").call({}).should == [1]
121
+ update_app("module App; def self.call(env) @a end; @a ||= []; @a << 2; end")
122
+ ru.call({}).should == [1, 2]
123
+ log_match(%r{\ALoading.*spec/app\.rb\z}, %r{\AReloading.*spec/app\.rb\z})
124
+ end
125
+
126
+ it "it unload partially loaded modules if loading fails, and allow future loading" do
127
+ ru.call({}).should == [1]
128
+ update_app("module App; def self.call(env) @a end; @a ||= []; raise 'foo'; end")
129
+ proc{ru.call({})}.should raise_error
130
+ defined?(::App).should == nil
131
+ update_app(code(2))
132
+ ru.call({}).should == [2]
133
+ log_match(%r{\ALoading.*spec/app\.rb\z}, "Removed constant App", %r{\AReloading.*spec/app\.rb\z},
134
+ %r{\AFailed to load .*spec/app\.rb; removing partially defined constants\z},
135
+ "Removed constant App", %r{\AReloading.*spec/app\.rb\z})
136
+ end
137
+
138
+ it "it should unload classes in namespaces" do
139
+ ru(:code=>"class Array::App; def self.call(env) @a end; @a ||= []; @a << 1; end", :block=>proc{Array::App}).call({}).should == [1]
140
+ update_app("class Array::App; def self.call(env) @a end; @a ||= []; @a << 2; end")
141
+ ru.call({}).should == [2]
142
+ log_match(%r{\ALoading.*spec/app\.rb\z}, "Removed constant Array::App", %r{\AReloading.*spec/app\.rb\z})
143
+ end
144
+
145
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-unreloader
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ platform: ruby
6
+ authors:
7
+ - Jeremy Evans
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-08-04 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |
14
+ Rack::Unreloader is a rack middleware that reloads application files when it
15
+ detects changes, unloading constants defined in those files before reloading.
16
+ email: code@jeremyevans.net
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files:
20
+ - README.rdoc
21
+ - CHANGELOG
22
+ - MIT-LICENSE
23
+ files:
24
+ - CHANGELOG
25
+ - MIT-LICENSE
26
+ - README.rdoc
27
+ - Rakefile
28
+ - lib/rack/unreloader.rb
29
+ - spec/unreloader_spec.rb
30
+ homepage: http://gihub.com/jeremyevans/rack-unreloader
31
+ licenses:
32
+ - MIT
33
+ metadata: {}
34
+ post_install_message:
35
+ rdoc_options:
36
+ - "--quiet"
37
+ - "--line-numbers"
38
+ - "--inline-source"
39
+ - "--title"
40
+ - 'Rack::Unreloader: Reload application when files change, unloading constants first'
41
+ - "--main"
42
+ - README.rdoc
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubyforge_project:
57
+ rubygems_version: 2.2.2
58
+ signing_key:
59
+ specification_version: 4
60
+ summary: Reload application when files change, unloading constants first
61
+ test_files: []