rack-unreloader 0.9.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.
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: []