rack-unreloader 1.1.0 → 1.2.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 +4 -4
- data/CHANGELOG +18 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +81 -25
- data/lib/rack/unreloader.rb +79 -298
- data/lib/rack/unreloader/reloader.rb +399 -0
- data/spec/unreloader_spec.rb +165 -6
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 37c98d1ba331382609dbc5b74058d02f6f194bc2
|
4
|
+
data.tar.gz: 77ae22b4fd6bd6a55f3ceb44d21e632d1fc87a77
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 41a3897cfab23c354c236f64c6ad9e6134c3170d2a6b4237a645a9ddc2ca8171bb99208dd11264402117b22f47682b3500590e709577a5fe747eab1536053120
|
7
|
+
data.tar.gz: f87b6f3e092f3341bb7bb82b20e2ace71ad3e424b4b676d5a481ad9dc724994fefea012967b202c4cf4a24fb083e50856c3209d15973b2e16961757bcb4d585d
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,21 @@
|
|
1
|
+
= 1.2.0 (2015-01-24)
|
2
|
+
|
3
|
+
* Add Unreloader#record_split_class, for handling classes split into multiple files (jeremyevans)
|
4
|
+
|
5
|
+
* Add Unreloader#reload!, for manually checking for changed files, useful for non-rack applications (jeremyevans)
|
6
|
+
|
7
|
+
* Make most methods in the Reloader private (jeremyevans)
|
8
|
+
|
9
|
+
* Separate reloading logic into separate file, so using unreloader in production takes less memory (jeremyevans)
|
10
|
+
|
11
|
+
* Add the ability to record dependencies, making it possible to correctly support modules and superclasses (jeremyevans)
|
12
|
+
|
13
|
+
* Add :reload option, set to false to not support reloading, useful in production mode (jeremyevans)
|
14
|
+
|
15
|
+
* Document that setting the :cooldown option to nil makes the Unreloader not reload (celsworth, jeremyevans) (#3)
|
16
|
+
|
17
|
+
* Handle files in subdirectories when monitoring directories (celsworth) (#2)
|
18
|
+
|
1
19
|
= 1.1.0 (2014-09-25)
|
2
20
|
|
3
21
|
* Allow monitoring of directories, so that new files can be picked up, and deleted files removed (jeremyevans)
|
data/MIT-LICENSE
CHANGED
data/README.rdoc
CHANGED
@@ -97,28 +97,24 @@ before requiring +app.rb+:
|
|
97
97
|
|
98
98
|
This way, changing your +app.rb+ file will not reload your +models.rb+ file.
|
99
99
|
|
100
|
-
== Only in Development Mode
|
101
|
-
|
102
|
-
In general, you are only going to want to
|
103
|
-
|
104
|
-
+
|
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
|
100
|
+
== Only Reload in Development Mode
|
101
|
+
|
102
|
+
In general, you are only going to want to reload code in development mode.
|
103
|
+
To simplify things, you can use rack-unreloader both in development and
|
104
|
+
production, and just not have it reload in production by setting +:reload+
|
105
|
+
to false if not in development:
|
116
106
|
|
117
|
-
|
118
|
-
|
119
|
-
|
107
|
+
dev = ENV['RACK_ENV'] == 'development'
|
108
|
+
require 'rack/unreloader'
|
109
|
+
Unreloader = Rack::Unreloader.new(:reload=>dev){App}
|
110
|
+
Unreloader.require './models.rb'
|
111
|
+
Unreloader.require './app.rb'
|
112
|
+
run(dev ? Unreloader : App)
|
120
113
|
|
121
|
-
|
114
|
+
By running the App instead of Unreloader in production mode, there is no
|
115
|
+
performance penalty. The advantage of this approach is you can use
|
116
|
+
Unreloader.require to require files regardless of whether you are using
|
117
|
+
development or production mode.
|
122
118
|
|
123
119
|
== Modules
|
124
120
|
|
@@ -128,9 +124,61 @@ need to specify the module name if you want to reload it:
|
|
128
124
|
|
129
125
|
Unreloader = Rack::Unreloader.new(:subclasses=>%w'MyModule'){App}
|
130
126
|
|
131
|
-
|
132
|
-
|
133
|
-
|
127
|
+
== Dependencies
|
128
|
+
|
129
|
+
To correctly handle modules and superclasses, if a change is made to a module
|
130
|
+
or superclass, you generally want to reload all classes that include the
|
131
|
+
module or subclass the superclass, so they they pick up the change to the
|
132
|
+
module or superclass.
|
133
|
+
|
134
|
+
You can specify the file dependencies when using rack-unreloader:
|
135
|
+
|
136
|
+
Unreload.record_dependency('lib/module_file.rb', %w'models/mod1.rb models/mod2.rb')
|
137
|
+
|
138
|
+
If lib/module_file.rb is changed, rack-unreloader will reload models/mod1.rb
|
139
|
+
and models/mod2.rb after reloading lib/module_file.rb.
|
140
|
+
|
141
|
+
You can provide directories when requiring dependencies. For example:
|
142
|
+
|
143
|
+
Unreload.record_dependency('helpers', %w'app.rb')
|
144
|
+
|
145
|
+
will make it so the addition of any ruby files to the helpers directory
|
146
|
+
will trigger a reload of +app.rb+, and future changes to any of those files
|
147
|
+
will also trigger of reload of +app.rb+. Additionally, deleting any ruby files
|
148
|
+
in the helpers directory will also trigger a reload of +app.rb+.
|
149
|
+
|
150
|
+
You can also use a directory as the second argument:
|
151
|
+
|
152
|
+
Unreload.record_dependency('mod.rb', 'models')
|
153
|
+
|
154
|
+
With this, any change to +mod.rb+ will trigger a reload of all ruby files in
|
155
|
+
the models directory, even if such files are added later.
|
156
|
+
|
157
|
+
When using +record_dependencies+ with a directory, you should also call
|
158
|
+
+require+ with that directory, as opposed to specifically requiring
|
159
|
+
individual files inside the directory.
|
160
|
+
|
161
|
+
== Classes Split Into Multiple Files
|
162
|
+
|
163
|
+
Rack::Unreloader handles classes split into multiple files, where there is
|
164
|
+
a main file for the class that requires the other files that define the
|
165
|
+
class. Assuming the main class file is +app.rb+, and other files that
|
166
|
+
make up the class are in +helpers+:
|
167
|
+
|
168
|
+
# inside config.ru
|
169
|
+
Unreloader.require 'app.rb'
|
170
|
+
|
171
|
+
# inside app.rb
|
172
|
+
Unreloader.require 'helpers'
|
173
|
+
Unreloader.record_split_class(__FILE__, 'helpers')
|
174
|
+
|
175
|
+
If +app.rb+ is changed or any of the ruby files in +helpers+ is changed,
|
176
|
+
it will reload +app.rb+ and all of the files in +helpers+. This makes
|
177
|
+
it so if you remove a method from one of the files in +helpers+, it will
|
178
|
+
reload the entire class so that the method is no longer defined. Likewise,
|
179
|
+
if you delete one of the files in helpers, it will reload the class so that
|
180
|
+
the methods that were defined in that file will no longer be defined on the
|
181
|
+
class.
|
134
182
|
|
135
183
|
== Requiring
|
136
184
|
|
@@ -157,14 +205,14 @@ the directory path:
|
|
157
205
|
|
158
206
|
The advantage for doing this is that new files added to the directory will be
|
159
207
|
picked up automatically, and files deleted from the directory will be removed
|
160
|
-
automatically.
|
208
|
+
automatically. This applies to files in subdirectories of that directory as well.
|
161
209
|
|
162
210
|
== Speeding Things Up
|
163
211
|
|
164
212
|
By default, <tt>Rack::Unreloader</tt> uses +ObjectSpace+ before and after requiring each
|
165
213
|
file that it monitors, to see which classes and modules were defined by the
|
166
214
|
require. This is slow for large numbers of files. In general use it isn't an
|
167
|
-
issue as
|
215
|
+
issue as generally only a single file will be changed at a time, but it can
|
168
216
|
significantly slow down startup when all files are being loaded at the same
|
169
217
|
time.
|
170
218
|
|
@@ -176,6 +224,14 @@ your models just use a capitalized version of the filename:
|
|
176
224
|
|
177
225
|
Unreloader.require('models'){|f| File.basename(f).sub(/\.rb\z/, '').capitalize}
|
178
226
|
|
227
|
+
== Usage Outside Rack
|
228
|
+
|
229
|
+
While <tt>Rack::Unreloader</tt> is usually in the development of rack applications,
|
230
|
+
it doesn't depend on rack. You can just instantiate an instance of Unreloader and
|
231
|
+
use it to handle reloading in any ruby application, just by using the +require+ and
|
232
|
+
+record_dependency+ to set up the metadata, and calling +reload!+ manually to
|
233
|
+
reload the application.
|
234
|
+
|
179
235
|
== History
|
180
236
|
|
181
237
|
Rack::Unreloader was derived from Padrino's reloader. It is significantly smaller
|
data/lib/rack/unreloader.rb
CHANGED
@@ -1,309 +1,50 @@
|
|
1
|
-
require '
|
1
|
+
require 'find'
|
2
2
|
|
3
3
|
module Rack
|
4
4
|
# Reloading application that unloads constants before reloading the relevant
|
5
5
|
# files, calling the new rack app if it gets reloaded.
|
6
6
|
class Unreloader
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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 of directories being monitored for changes, keyed by absolute path of directory name,
|
25
|
-
# with values being the an array with the last modified time (or nil if the directory has not
|
26
|
-
# yet been loaded), an array of files in the directory, and a block to pass to
|
27
|
-
# require_dependency for new files.
|
28
|
-
@monitor_dirs = {}
|
29
|
-
|
30
|
-
# Hash of procs returning constants defined in files, keyed by absolute path
|
31
|
-
# of file name. If there is no proc, must call ObjectSpace before and after
|
32
|
-
# loading files to detect changes, which is slower.
|
33
|
-
@constants_defined = {}
|
34
|
-
|
35
|
-
# Hash keyed by absolute path of file name, storing constants and other
|
36
|
-
# filenames that the key loads. Values should be hashes with :constants
|
37
|
-
# and :features keys, and arrays of values.
|
38
|
-
@files = {}
|
39
|
-
|
40
|
-
# Similar to @files, but stores previous entries, used when rolling back.
|
41
|
-
@old_entries = {}
|
42
|
-
end
|
43
|
-
|
44
|
-
# Tries to find a declared constant with the name specified
|
45
|
-
# in the string. It raises a NameError when the name is not in CamelCase
|
46
|
-
# or is not initialized.
|
47
|
-
def constantize(s)
|
48
|
-
s = s.to_s
|
49
|
-
if m = VALID_CONSTANT_NAME_REGEXP.match(s)
|
50
|
-
Object.module_eval("::#{m[1]}", __FILE__, __LINE__)
|
51
|
-
else
|
52
|
-
log("#{s.inspect} is not a valid constant name!")
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
# Log the given string at info level if there is a logger.
|
57
|
-
def log(s)
|
58
|
-
@logger.info(s) if @logger
|
59
|
-
end
|
60
|
-
|
61
|
-
# If there are any changed files, reload them. If there are no changed
|
62
|
-
# files, do nothing.
|
63
|
-
def reload!
|
64
|
-
@monitor_dirs.keys.each do |dir|
|
65
|
-
check_monitor_dir(dir)
|
66
|
-
end
|
67
|
-
|
68
|
-
@monitor_files.to_a.each do |file, time|
|
69
|
-
if file_changed?(file, time)
|
70
|
-
safe_load(file)
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
# Check a monitored directory for changes, adding new files and removing
|
76
|
-
# deleted files.
|
77
|
-
def check_monitor_dir(dir)
|
78
|
-
time, files, block = @monitor_dirs[dir]
|
79
|
-
|
80
|
-
if file_changed?(dir, time)
|
81
|
-
cur_files = Dir.new(dir).grep(/\.rb\z/).map{|f| F.join(dir, f)}
|
82
|
-
|
83
|
-
(files - cur_files).each do |f|
|
84
|
-
remove(f)
|
85
|
-
@monitor_files.delete(f)
|
86
|
-
end
|
87
|
-
|
88
|
-
require_dependencies(cur_files - files, &block)
|
89
|
-
|
90
|
-
files.replace(cur_files)
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
# Require the given dependencies, monitoring them for changes.
|
95
|
-
# Paths should be a file glob or an array of file globs.
|
96
|
-
def require_dependencies(paths, &block)
|
97
|
-
options = {:cyclic => true}
|
98
|
-
error = nil
|
99
|
-
|
100
|
-
Array(paths).
|
101
|
-
flatten.
|
102
|
-
map{|path| Dir.glob(path).sort_by{|filename| filename.count('/')}}.
|
103
|
-
flatten.
|
104
|
-
map{|path| F.expand_path(path)}.
|
105
|
-
uniq.
|
106
|
-
each do |file|
|
107
|
-
|
108
|
-
if F.directory?(file)
|
109
|
-
@monitor_dirs[file] = [nil, [], block]
|
110
|
-
check_monitor_dir(file)
|
111
|
-
next
|
112
|
-
else
|
113
|
-
@constants_defined[file] = block
|
114
|
-
@monitor_files[file] = nil
|
115
|
-
end
|
116
|
-
|
117
|
-
begin
|
118
|
-
safe_load(file, options)
|
119
|
-
rescue NameError, LoadError => error
|
120
|
-
log "Cyclic dependency reload for #{error}"
|
121
|
-
rescue Exception => error
|
122
|
-
break
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
if error
|
127
|
-
log error
|
128
|
-
raise error
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
# Requires the given file, logging which constants or features are added
|
133
|
-
# by the require, and rolling back the constants and features if there
|
134
|
-
# are any errors.
|
135
|
-
def safe_load(file, options={})
|
136
|
-
return unless @monitor_files.has_key?(file)
|
137
|
-
return unless options[:force] || file_changed?(file)
|
138
|
-
|
139
|
-
log "#{@monitor_files[file] ? 'Reloading' : 'Loading'} #{file}"
|
140
|
-
prepare(file) # might call #safe_load recursively
|
141
|
-
begin
|
142
|
-
require(file)
|
143
|
-
commit(file)
|
144
|
-
rescue Exception
|
145
|
-
if !options[:cyclic]
|
146
|
-
log "Failed to load #{file}; removing partially defined constants"
|
147
|
-
end
|
148
|
-
rollback(file)
|
149
|
-
raise
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
|
-
# Removes the specified constant.
|
154
|
-
def remove_constant(const)
|
155
|
-
base, _, object = const.to_s.rpartition('::')
|
156
|
-
base = base.empty? ? Object : constantize(base)
|
157
|
-
base.send :remove_const, object
|
158
|
-
log "Removed constant #{const}"
|
159
|
-
rescue NameError
|
160
|
-
log "Error removing constant: #{const}"
|
161
|
-
end
|
162
|
-
|
163
|
-
# Remove a feature if it is being monitored for reloading, so it
|
164
|
-
# can be required again.
|
165
|
-
def remove_feature(file)
|
166
|
-
if @monitor_files.has_key?(file)
|
167
|
-
$LOADED_FEATURES.delete(file)
|
168
|
-
log "Removed feature #{file}"
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
# Unload all reloadable constants and features, and clear the list
|
173
|
-
# of files to monitor.
|
174
|
-
def clear!
|
175
|
-
@files.keys.each do |file|
|
176
|
-
remove(file)
|
177
|
-
end
|
178
|
-
@monitor_files = {}
|
179
|
-
@old_entries = {}
|
180
|
-
end
|
181
|
-
|
182
|
-
# Remove the given file, removing any constants and other files loaded
|
183
|
-
# by the file.
|
184
|
-
def remove(name)
|
185
|
-
file = @files[name] || return
|
186
|
-
remove_constants(name){file[:constants]}
|
187
|
-
file[:features].each{|feature| remove_feature(feature)}
|
188
|
-
@files.delete(name)
|
189
|
-
remove_feature(name) if $LOADED_FEATURES.include?(name)
|
190
|
-
end
|
191
|
-
|
192
|
-
# Remove constants defined in file. Uses the stored block if there is
|
193
|
-
# one for the file name, or the given block.
|
194
|
-
def remove_constants(name)
|
195
|
-
constants = if pr = @constants_defined[name]
|
196
|
-
Array(pr.call(name))
|
197
|
-
else
|
198
|
-
yield
|
199
|
-
end
|
200
|
-
|
201
|
-
if constants
|
202
|
-
constants.each{|constant| remove_constant(constant)}
|
203
|
-
end
|
204
|
-
end
|
205
|
-
|
206
|
-
# Store the currently loaded classes and features, so in case of an error
|
207
|
-
# this state can be rolled back to.
|
208
|
-
def prepare(name)
|
209
|
-
file = remove(name)
|
210
|
-
@old_entries[name] = {:features => monitored_features}
|
211
|
-
|
212
|
-
unless @constants_defined[name]
|
213
|
-
@old_entries[name][:constants] = all_classes
|
214
|
-
end
|
215
|
-
end
|
216
|
-
|
217
|
-
# Commit the changed state after requiring the the file, recording the new
|
218
|
-
# classes and features added by the file.
|
219
|
-
def commit(name)
|
220
|
-
entry = {:features => monitored_features - @old_entries[name][:features] - [name]}
|
221
|
-
unless constants_defined = @constants_defined[name]
|
222
|
-
entry[:constants] = new_classes(@old_entries[name][:constants])
|
223
|
-
end
|
224
|
-
|
225
|
-
@files[name] = entry
|
226
|
-
@old_entries.delete(name)
|
227
|
-
@monitor_files[name] = modified_at(name)
|
228
|
-
|
229
|
-
unless constants_defined
|
230
|
-
log("New classes in #{name}: #{entry[:constants].to_a.join(' ')}") unless entry[:constants].empty?
|
231
|
-
end
|
232
|
-
log("New features in #{name}: #{entry[:features].to_a.join(' ')}") unless entry[:features].empty?
|
233
|
-
end
|
234
|
-
|
235
|
-
# Rollback the changes made by requiring the file, restoring the previous state.
|
236
|
-
def rollback(name)
|
237
|
-
remove_constants(name){new_classes(@old_entries[name][:constants])}
|
238
|
-
@old_entries.delete(name)
|
239
|
-
end
|
240
|
-
|
241
|
-
private
|
242
|
-
|
243
|
-
# The current loaded features that are being monitored
|
244
|
-
def monitored_features
|
245
|
-
Set.new($LOADED_FEATURES) & @monitor_files.keys
|
246
|
-
end
|
247
|
-
|
248
|
-
# Return a set of all classes in the ObjectSpace.
|
249
|
-
def all_classes
|
250
|
-
rs = Set.new
|
251
|
-
|
252
|
-
ObjectSpace.each_object(Module).each do |mod|
|
253
|
-
if !mod.name.to_s.empty? && monitored_module?(mod)
|
254
|
-
rs << mod
|
255
|
-
end
|
256
|
-
end
|
257
|
-
|
258
|
-
rs
|
259
|
-
end
|
260
|
-
|
261
|
-
# Return whether the given klass is a monitored class that could
|
262
|
-
# be unloaded.
|
263
|
-
def monitored_module?(mod)
|
264
|
-
@classes.any? do |c|
|
265
|
-
c = constantize(c) rescue false
|
266
|
-
|
267
|
-
if mod.is_a?(Class)
|
268
|
-
# Reload the class if it is a subclass if the current class
|
269
|
-
(mod < c) rescue false
|
270
|
-
elsif c == Object
|
271
|
-
# If reloading for all classes, reload for all modules as well
|
272
|
-
true
|
273
|
-
else
|
274
|
-
# Otherwise, reload only if the module matches exactly, since
|
275
|
-
# modules don't have superclasses.
|
276
|
-
mod == c
|
277
|
-
end
|
278
|
-
end
|
279
|
-
end
|
280
|
-
|
281
|
-
# Return a set of all classes in the ObjectSpace that are not in the
|
282
|
-
# given set of classes.
|
283
|
-
def new_classes(snapshot)
|
284
|
-
all_classes - snapshot
|
285
|
-
end
|
7
|
+
# Reference to ::File as File would return Rack::File by default.
|
8
|
+
F = ::File
|
9
|
+
|
10
|
+
# Given the list of paths, find all matching files, or matching ruby files
|
11
|
+
# in subdirecories if given a directory, and return an array of expanded
|
12
|
+
# paths.
|
13
|
+
def self.expand_directory_paths(paths)
|
14
|
+
expand_paths(paths).
|
15
|
+
map{|f| F.directory?(f) ? ruby_files(f) : f}.
|
16
|
+
flatten
|
17
|
+
end
|
286
18
|
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
19
|
+
# Given the path glob or array of path globs, find all matching files
|
20
|
+
# or directories, and return an array of expanded paths.
|
21
|
+
def self.expand_paths(paths)
|
22
|
+
Array(paths).
|
23
|
+
flatten.
|
24
|
+
map{|path| Dir.glob(path).sort_by{|filename| filename.count('/')}}.
|
25
|
+
flatten.
|
26
|
+
map{|path| F.expand_path(path)}.
|
27
|
+
uniq
|
28
|
+
end
|
291
29
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
30
|
+
# The .rb files in the given directory or any subdirectory.
|
31
|
+
def self.ruby_files(dir)
|
32
|
+
files = []
|
33
|
+
Find.find(dir) do |f|
|
34
|
+
files << f if f =~ /\.rb\z/
|
297
35
|
end
|
36
|
+
files.sort
|
298
37
|
end
|
299
38
|
|
300
|
-
# The Rack::Unreloader::Reloader instead related to this instance.
|
39
|
+
# The Rack::Unreloader::Reloader instead related to this instance, if one.
|
301
40
|
attr_reader :reloader
|
302
41
|
|
303
42
|
# Setup the reloader. Options:
|
304
43
|
#
|
305
44
|
# :cooldown :: The number of seconds to wait between checks for changed files.
|
306
|
-
# Defaults to 1.
|
45
|
+
# Defaults to 1. Set to nil/false to not check for changed files.
|
46
|
+
# :reload :: Set to false to not setup a reloader, and just have require work
|
47
|
+
# directly. Should be set to false in production mode.
|
307
48
|
# :logger :: A Logger instance which will log information related to reloading.
|
308
49
|
# :subclasses :: A string or array of strings of class names that should be unloaded.
|
309
50
|
# Any classes that are not subclasses of these classes will not be
|
@@ -311,25 +52,65 @@ module Rack
|
|
311
52
|
# match exactly, since modules don't have superclasses.
|
312
53
|
def initialize(opts={}, &block)
|
313
54
|
@app_block = block
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
55
|
+
if opts.fetch(:reload, true)
|
56
|
+
@cooldown = opts.fetch(:cooldown, 1)
|
57
|
+
@last = Time.at(0)
|
58
|
+
Kernel.require 'rack/unreloader/reloader'
|
59
|
+
@reloader = Reloader.new(opts)
|
60
|
+
reload!
|
61
|
+
else
|
62
|
+
@reloader = @cooldown = false
|
63
|
+
end
|
318
64
|
end
|
319
65
|
|
320
66
|
# If the cooldown time has been passed, reload any application files that have changed.
|
321
67
|
# Call the app with the environment.
|
322
68
|
def call(env)
|
323
69
|
if @cooldown && Time.now > @last + @cooldown
|
324
|
-
Thread.respond_to?(:exclusive) ? Thread.exclusive{
|
70
|
+
Thread.respond_to?(:exclusive) ? Thread.exclusive{reload!} : reload!
|
325
71
|
@last = Time.now
|
326
72
|
end
|
327
73
|
@app_block.call.call(env)
|
328
74
|
end
|
329
75
|
|
330
76
|
# Add a file glob or array of file globs to monitor for changes.
|
331
|
-
def require(
|
332
|
-
@reloader
|
77
|
+
def require(paths, &block)
|
78
|
+
if @reloader
|
79
|
+
@reloader.require_dependencies(paths, &block)
|
80
|
+
else
|
81
|
+
Unreloader.expand_directory_paths(paths).each{|f| super(f)}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Records that each path in +files+ depends on +dependency+. If there
|
86
|
+
# is a modification to +dependency+, all related files will be reloaded
|
87
|
+
# after +dependency+ is reloaded. Both +dependency+ and each entry in +files+
|
88
|
+
# can be an array of path globs.
|
89
|
+
def record_dependency(dependency, *files)
|
90
|
+
if @reloader
|
91
|
+
files = Unreloader.expand_paths(files)
|
92
|
+
Unreloader.expand_paths(dependency).each do |path|
|
93
|
+
@reloader.record_dependency(path, files)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Record that a class is split into multiple files. +main_file+ should be
|
99
|
+
# the main file for the class, which should require all of the other
|
100
|
+
# files. +files+ should be a list of all other files that make up the class.
|
101
|
+
def record_split_class(main_file, *files)
|
102
|
+
if @reloader
|
103
|
+
files = Unreloader.expand_paths(files)
|
104
|
+
files.each do |file|
|
105
|
+
record_dependency(file, main_file)
|
106
|
+
end
|
107
|
+
@reloader.skip_reload(files)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Reload the application, checking for changed files and reloading them.
|
112
|
+
def reload!
|
113
|
+
@reloader.reload! if @reloader
|
333
114
|
end
|
334
115
|
end
|
335
116
|
end
|
@@ -0,0 +1,399 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class Unreloader
|
5
|
+
class Reloader
|
6
|
+
F = ::File
|
7
|
+
|
8
|
+
# Regexp for valid constant names, to prevent code execution.
|
9
|
+
VALID_CONSTANT_NAME_REGEXP = /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/.freeze
|
10
|
+
|
11
|
+
# Options hash to force loading of files even if they haven't changed.
|
12
|
+
FORCE = {:force=>true}.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 of directories being monitored for changes, keyed by absolute path of directory name,
|
25
|
+
# with values being the an array with the last modified time (or nil if the directory has not
|
26
|
+
# yet been loaded), an array of files in the directory, and a block to pass to
|
27
|
+
# require_dependency for new files.
|
28
|
+
@monitor_dirs = {}
|
29
|
+
|
30
|
+
# Hash of procs returning constants defined in files, keyed by absolute path
|
31
|
+
# of file name. If there is no proc, must call ObjectSpace before and after
|
32
|
+
# loading files to detect changes, which is slower.
|
33
|
+
@constants_defined = {}
|
34
|
+
|
35
|
+
# Hash keyed by absolute path of file name, storing constants and other
|
36
|
+
# filenames that the key loads. Values should be hashes with :constants
|
37
|
+
# and :features keys, and arrays of values.
|
38
|
+
@files = {}
|
39
|
+
|
40
|
+
# Similar to @files, but stores previous entries, used when rolling back.
|
41
|
+
@old_entries = {}
|
42
|
+
|
43
|
+
# Records dependencies on files. Keys are absolute paths, values are arrays of absolute paths,
|
44
|
+
# where each entry depends on the key, so that if the key path is modified, all values are
|
45
|
+
# reloaded.
|
46
|
+
@dependencies = {}
|
47
|
+
|
48
|
+
# Array of the order in which to load dependencies
|
49
|
+
@dependency_order = []
|
50
|
+
|
51
|
+
# Array of absolute paths which should be unloaded, but not reloaded on changes,
|
52
|
+
# because files that depend on them will load them automatically.
|
53
|
+
@skip_reload = []
|
54
|
+
end
|
55
|
+
|
56
|
+
# Unload all reloadable constants and features, and clear the list
|
57
|
+
# of files to monitor.
|
58
|
+
def clear!
|
59
|
+
@files.keys.each do |file|
|
60
|
+
remove(file)
|
61
|
+
end
|
62
|
+
@monitor_files = {}
|
63
|
+
@old_entries = {}
|
64
|
+
end
|
65
|
+
|
66
|
+
# Record a dependency the given files, such that each file in +files+
|
67
|
+
# depends on +path+. If +path+ changes, each file in +files+ should
|
68
|
+
# be reloaded as well.
|
69
|
+
def record_dependency(path, files)
|
70
|
+
files = (@dependencies[path] ||= []).concat(files)
|
71
|
+
files.uniq!
|
72
|
+
|
73
|
+
order = @dependency_order
|
74
|
+
i = order.find_index{|v| files.include?(v)} || -1
|
75
|
+
order.insert(i, path)
|
76
|
+
order.concat(files)
|
77
|
+
order.uniq!
|
78
|
+
|
79
|
+
if F.directory?(path)
|
80
|
+
(@monitor_files.keys & Unreloader.ruby_files(path)).each do |file|
|
81
|
+
record_dependency(file, files)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
|
88
|
+
# If there are any changed files, reload them. If there are no changed
|
89
|
+
# files, do nothing.
|
90
|
+
def reload!
|
91
|
+
changed_files = []
|
92
|
+
|
93
|
+
@monitor_dirs.keys.each do |dir|
|
94
|
+
check_monitor_dir(dir, changed_files)
|
95
|
+
end
|
96
|
+
|
97
|
+
@monitor_files.each do |file, time|
|
98
|
+
if file_changed?(file, time)
|
99
|
+
changed_files << file
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
return if changed_files.empty?
|
104
|
+
|
105
|
+
unless @dependencies.empty?
|
106
|
+
changed_files = reload_files(changed_files)
|
107
|
+
changed_files.flatten!
|
108
|
+
changed_files.map!{|f| F.directory?(f) ? Unreloader.ruby_files(f) : f}
|
109
|
+
changed_files.flatten!
|
110
|
+
changed_files.uniq!
|
111
|
+
|
112
|
+
order = @dependency_order
|
113
|
+
order &= changed_files
|
114
|
+
changed_files = order + (changed_files - order)
|
115
|
+
end
|
116
|
+
|
117
|
+
unless @skip_reload.empty?
|
118
|
+
skip_reload = @skip_reload.map{|f| F.directory?(f) ? Unreloader.ruby_files(f) : f}
|
119
|
+
skip_reload.flatten!
|
120
|
+
skip_reload.uniq!
|
121
|
+
changed_files -= skip_reload
|
122
|
+
end
|
123
|
+
|
124
|
+
changed_files.each do |file|
|
125
|
+
safe_load(file, FORCE)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Require the given dependencies, monitoring them for changes.
|
130
|
+
# Paths should be a file glob or an array of file globs.
|
131
|
+
def require_dependencies(paths, &block)
|
132
|
+
options = {:cyclic => true}
|
133
|
+
error = nil
|
134
|
+
|
135
|
+
Unreloader.expand_paths(paths).each do |file|
|
136
|
+
if F.directory?(file)
|
137
|
+
@monitor_dirs[file] = [nil, [], block]
|
138
|
+
check_monitor_dir(file)
|
139
|
+
next
|
140
|
+
else
|
141
|
+
@constants_defined[file] = block
|
142
|
+
@monitor_files[file] = nil
|
143
|
+
end
|
144
|
+
|
145
|
+
begin
|
146
|
+
safe_load(file, options)
|
147
|
+
rescue NameError, LoadError => error
|
148
|
+
log "Cyclic dependency reload for #{error}"
|
149
|
+
rescue Exception => error
|
150
|
+
break
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
if error
|
155
|
+
log error
|
156
|
+
raise error
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Skip reloading the given files. Should only be used if other files
|
161
|
+
# depend on these files and the other files require these files when
|
162
|
+
# loaded.
|
163
|
+
def skip_reload(files)
|
164
|
+
@skip_reload.concat(files)
|
165
|
+
@skip_reload.uniq!
|
166
|
+
nil
|
167
|
+
end
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
# Tries to find a declared constant with the name specified
|
172
|
+
# in the string. It raises a NameError when the name is not in CamelCase
|
173
|
+
# or is not initialized.
|
174
|
+
def constantize(s)
|
175
|
+
s = s.to_s
|
176
|
+
if m = VALID_CONSTANT_NAME_REGEXP.match(s)
|
177
|
+
Object.module_eval("::#{m[1]}", __FILE__, __LINE__)
|
178
|
+
else
|
179
|
+
log("#{s.inspect} is not a valid constant name!")
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Log the given string at info level if there is a logger.
|
184
|
+
def log(s)
|
185
|
+
@logger.info(s) if @logger
|
186
|
+
end
|
187
|
+
|
188
|
+
# Check a monitored directory for changes, adding new files and removing
|
189
|
+
# deleted files.
|
190
|
+
def check_monitor_dir(dir, changed_files=nil)
|
191
|
+
time, files, block = @monitor_dirs[dir]
|
192
|
+
|
193
|
+
cur_files = Unreloader.ruby_files(dir)
|
194
|
+
return if files == cur_files
|
195
|
+
|
196
|
+
removed_files = files - cur_files
|
197
|
+
new_files = cur_files - files
|
198
|
+
|
199
|
+
if changed_files
|
200
|
+
changed_files.concat(dependency_files(removed_files))
|
201
|
+
end
|
202
|
+
|
203
|
+
removed_files.each do |f|
|
204
|
+
remove(f)
|
205
|
+
@monitor_files.delete(f)
|
206
|
+
@dependencies.delete(f)
|
207
|
+
@dependency_order.delete(f)
|
208
|
+
end
|
209
|
+
|
210
|
+
require_dependencies(new_files, &block)
|
211
|
+
|
212
|
+
new_files.each do |file|
|
213
|
+
if deps = @dependencies[dir]
|
214
|
+
record_dependency(file, deps)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
if changed_files
|
219
|
+
changed_files.concat(dependency_files(new_files))
|
220
|
+
end
|
221
|
+
|
222
|
+
files.replace(cur_files)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Requires the given file, logging which constants or features are added
|
226
|
+
# by the require, and rolling back the constants and features if there
|
227
|
+
# are any errors.
|
228
|
+
def safe_load(file, options={})
|
229
|
+
return unless @monitor_files.has_key?(file)
|
230
|
+
return unless options[:force] || file_changed?(file)
|
231
|
+
|
232
|
+
log "#{@monitor_files[file] ? 'Reloading' : 'Loading'} #{file}"
|
233
|
+
prepare(file) # might call #safe_load recursively
|
234
|
+
begin
|
235
|
+
require(file)
|
236
|
+
commit(file)
|
237
|
+
rescue Exception
|
238
|
+
if !options[:cyclic]
|
239
|
+
log "Failed to load #{file}; removing partially defined constants"
|
240
|
+
end
|
241
|
+
rollback(file)
|
242
|
+
raise
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Removes the specified constant.
|
247
|
+
def remove_constant(const)
|
248
|
+
base, _, object = const.to_s.rpartition('::')
|
249
|
+
base = base.empty? ? Object : constantize(base)
|
250
|
+
base.send :remove_const, object
|
251
|
+
log "Removed constant #{const}"
|
252
|
+
rescue NameError
|
253
|
+
log "Error removing constant: #{const}"
|
254
|
+
end
|
255
|
+
|
256
|
+
# Remove a feature if it is being monitored for reloading, so it
|
257
|
+
# can be required again.
|
258
|
+
def remove_feature(file)
|
259
|
+
if @monitor_files.has_key?(file)
|
260
|
+
$LOADED_FEATURES.delete(file)
|
261
|
+
log "Removed feature #{file}"
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# Remove the given file, removing any constants and other files loaded
|
266
|
+
# by the file.
|
267
|
+
def remove(name)
|
268
|
+
file = @files[name] || return
|
269
|
+
remove_constants(name){file[:constants]}
|
270
|
+
file[:features].each{|feature| remove_feature(feature)}
|
271
|
+
@files.delete(name)
|
272
|
+
remove_feature(name) if $LOADED_FEATURES.include?(name)
|
273
|
+
end
|
274
|
+
|
275
|
+
# Remove constants defined in file. Uses the stored block if there is
|
276
|
+
# one for the file name, or the given block.
|
277
|
+
def remove_constants(name)
|
278
|
+
constants = if pr = @constants_defined[name]
|
279
|
+
Array(pr.call(name))
|
280
|
+
else
|
281
|
+
yield
|
282
|
+
end
|
283
|
+
|
284
|
+
if constants
|
285
|
+
constants.each{|constant| remove_constant(constant)}
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
# Store the currently loaded classes and features, so in case of an error
|
290
|
+
# this state can be rolled back to.
|
291
|
+
def prepare(name)
|
292
|
+
file = remove(name)
|
293
|
+
@old_entries[name] = {:features => monitored_features}
|
294
|
+
|
295
|
+
unless @constants_defined[name]
|
296
|
+
@old_entries[name][:constants] = all_classes
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
# Commit the changed state after requiring the the file, recording the new
|
301
|
+
# classes and features added by the file.
|
302
|
+
def commit(name)
|
303
|
+
entry = {:features => monitored_features - @old_entries[name][:features] - [name]}
|
304
|
+
unless constants_defined = @constants_defined[name]
|
305
|
+
entry[:constants] = new_classes(@old_entries[name][:constants])
|
306
|
+
end
|
307
|
+
|
308
|
+
@files[name] = entry
|
309
|
+
@old_entries.delete(name)
|
310
|
+
@monitor_files[name] = modified_at(name)
|
311
|
+
|
312
|
+
unless constants_defined
|
313
|
+
log("New classes in #{name}: #{entry[:constants].to_a.join(' ')}") unless entry[:constants].empty?
|
314
|
+
end
|
315
|
+
log("New features in #{name}: #{entry[:features].to_a.join(' ')}") unless entry[:features].empty?
|
316
|
+
end
|
317
|
+
|
318
|
+
# Rollback the changes made by requiring the file, restoring the previous state.
|
319
|
+
def rollback(name)
|
320
|
+
remove_constants(name){new_classes(@old_entries[name][:constants])}
|
321
|
+
@old_entries.delete(name)
|
322
|
+
end
|
323
|
+
|
324
|
+
# The current loaded features that are being monitored
|
325
|
+
def monitored_features
|
326
|
+
Set.new($LOADED_FEATURES) & @monitor_files.keys
|
327
|
+
end
|
328
|
+
|
329
|
+
# Return a set of all classes in the ObjectSpace.
|
330
|
+
def all_classes
|
331
|
+
rs = Set.new
|
332
|
+
|
333
|
+
ObjectSpace.each_object(Module).each do |mod|
|
334
|
+
if !mod.name.to_s.empty? && monitored_module?(mod)
|
335
|
+
rs << mod
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
rs
|
340
|
+
end
|
341
|
+
|
342
|
+
# Return whether the given klass is a monitored class that could
|
343
|
+
# be unloaded.
|
344
|
+
def monitored_module?(mod)
|
345
|
+
@classes.any? do |c|
|
346
|
+
c = constantize(c) rescue false
|
347
|
+
|
348
|
+
if mod.is_a?(Class)
|
349
|
+
# Reload the class if it is a subclass if the current class
|
350
|
+
(mod < c) rescue false
|
351
|
+
elsif c == Object
|
352
|
+
# If reloading for all classes, reload for all modules as well
|
353
|
+
true
|
354
|
+
else
|
355
|
+
# Otherwise, reload only if the module matches exactly, since
|
356
|
+
# modules don't have superclasses.
|
357
|
+
mod == c
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
# Recursively reload dependencies for the changed files.
|
363
|
+
def reload_files(changed_files)
|
364
|
+
changed_files.map do |file|
|
365
|
+
if deps = @dependencies[file]
|
366
|
+
[file] + reload_files(deps)
|
367
|
+
else
|
368
|
+
file
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
# The dependencies for the changed files, excluding the files themselves.
|
374
|
+
def dependency_files(changed_files)
|
375
|
+
files = reload_files(changed_files)
|
376
|
+
files.flatten!
|
377
|
+
files - changed_files
|
378
|
+
end
|
379
|
+
|
380
|
+
# Return a set of all classes in the ObjectSpace that are not in the
|
381
|
+
# given set of classes.
|
382
|
+
def new_classes(snapshot)
|
383
|
+
all_classes - snapshot
|
384
|
+
end
|
385
|
+
|
386
|
+
# Returns true if the file is new or it's modification time changed.
|
387
|
+
def file_changed?(file, time = @monitor_files[file])
|
388
|
+
!time || modified_at(file) > time
|
389
|
+
end
|
390
|
+
|
391
|
+
# Return the time the file was modified at. This can be overridden
|
392
|
+
# to base the reloading on something other than the file's modification
|
393
|
+
# time.
|
394
|
+
def modified_at(file)
|
395
|
+
F.mtime(file)
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
data/spec/unreloader_spec.rb
CHANGED
@@ -33,7 +33,7 @@ describe Rack::Unreloader do
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def update_app(code, file=@filename)
|
36
|
-
ru.reloader.set_modified_time(file, @i += 1)
|
36
|
+
ru.reloader.set_modified_time(file, @i += 1) if ru.reloader
|
37
37
|
File.open(file, 'wb'){|f| f.write(code)}
|
38
38
|
end
|
39
39
|
|
@@ -49,7 +49,7 @@ describe Rack::Unreloader do
|
|
49
49
|
def base_ru(opts={})
|
50
50
|
block = opts[:block] || proc{App}
|
51
51
|
@ru = Rack::Unreloader.new({:logger=>logger, :cooldown=>0}.merge(opts), &block)
|
52
|
-
@ru.reloader.extend ModifiedAt
|
52
|
+
@ru.reloader.extend ModifiedAt if @ru.reloader
|
53
53
|
Object.const_set(:RU, @ru)
|
54
54
|
end
|
55
55
|
|
@@ -57,7 +57,7 @@ describe Rack::Unreloader do
|
|
57
57
|
return @ru if @ru
|
58
58
|
base_ru(opts)
|
59
59
|
update_app(opts[:code]||code(1))
|
60
|
-
@ru.require
|
60
|
+
@ru.require @filename
|
61
61
|
@ru
|
62
62
|
end
|
63
63
|
|
@@ -72,13 +72,30 @@ describe Rack::Unreloader do
|
|
72
72
|
end
|
73
73
|
|
74
74
|
after do
|
75
|
-
ru.reloader.clear!
|
75
|
+
ru.reloader.clear! if ru.reloader
|
76
76
|
Object.send(:remove_const, :RU)
|
77
77
|
Object.send(:remove_const, :App) if defined?(::App)
|
78
78
|
Object.send(:remove_const, :App2) if defined?(::App2)
|
79
79
|
Dir['spec/app*.rb'].each{|f| File.delete(f)}
|
80
80
|
end
|
81
81
|
|
82
|
+
it "should not reload files automatically if cooldown option is nil" do
|
83
|
+
ru(:cooldown => nil).call({}).should == [1]
|
84
|
+
update_app(code(2))
|
85
|
+
ru.call({}).should == [1]
|
86
|
+
@ru.reload!
|
87
|
+
ru.call({}).should == [2]
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should not setup a reloader if reload option is false" do
|
91
|
+
@filename = 'spec/app_no_reload.rb'
|
92
|
+
ru(:reload => false).call({}).should == [1]
|
93
|
+
file = 'spec/app_no_reload2.rb'
|
94
|
+
File.open(file, 'wb'){|f| f.write('ANR2 = 2')}
|
95
|
+
ru.require 'spec/app_no_*2.rb'
|
96
|
+
ANR2.should == 2
|
97
|
+
end
|
98
|
+
|
82
99
|
it "should unload constants contained in file and reload file if file changes" do
|
83
100
|
ru.call({}).should == [1]
|
84
101
|
update_app(code(2))
|
@@ -268,16 +285,113 @@ describe Rack::Unreloader do
|
|
268
285
|
%r{\ARemoved feature .*/spec/app.rb\z}
|
269
286
|
end
|
270
287
|
|
288
|
+
it "should handle recorded dependencies" do
|
289
|
+
base_ru
|
290
|
+
update_app("module A; B = 1; end", 'spec/app_mod.rb')
|
291
|
+
update_app("class App; A = ::A; def self.call(env) A::B end; end")
|
292
|
+
ru.require 'spec/app_mod.rb'
|
293
|
+
ru.require 'spec/app.rb'
|
294
|
+
ru.record_dependency 'spec/app_mod.rb', 'spec/app.rb'
|
295
|
+
ru.call({}).should == 1
|
296
|
+
update_app("module A; B = 2; end", 'spec/app_mod.rb')
|
297
|
+
ru.call({}).should == 2
|
298
|
+
update_app("module A; include C; end", 'spec/app_mod.rb')
|
299
|
+
update_app("module C; B = 3; end", 'spec/app_mod2.rb')
|
300
|
+
ru.record_dependency 'spec/app_mod2.rb', 'spec/app_mod.rb'
|
301
|
+
ru.require 'spec/app_mod2.rb'
|
302
|
+
ru.call({}).should == 3
|
303
|
+
update_app("module C; B = 4; end", 'spec/app_mod2.rb')
|
304
|
+
ru.call({}).should == 4
|
305
|
+
end
|
306
|
+
|
271
307
|
describe "with a directory" do
|
272
|
-
before do
|
308
|
+
before(:all) do
|
273
309
|
Dir.mkdir('spec/dir')
|
310
|
+
Dir.mkdir('spec/dir/subdir')
|
311
|
+
Dir.mkdir('spec/dir/subdir2')
|
274
312
|
end
|
275
313
|
|
276
314
|
after do
|
277
|
-
Dir['spec/dir
|
315
|
+
Dir['spec/dir/**/*.rb'].each{|f| File.delete(f)}
|
316
|
+
end
|
317
|
+
|
318
|
+
after(:all) do
|
319
|
+
Dir.rmdir('spec/dir/subdir')
|
320
|
+
Dir.rmdir('spec/dir/subdir2')
|
278
321
|
Dir.rmdir('spec/dir')
|
279
322
|
end
|
280
323
|
|
324
|
+
it "should have unreloader require with directories if reload option is false" do
|
325
|
+
file = 'spec/dir/app_no_reload3.rb'
|
326
|
+
File.open(file, 'wb'){|f| f.write('ANR3 = 3')}
|
327
|
+
base_ru(:reload => false)
|
328
|
+
ru.require 'spec/dir'
|
329
|
+
ANR3.should == 3
|
330
|
+
end
|
331
|
+
|
332
|
+
it "should handle recorded dependencies in directories" do
|
333
|
+
base_ru
|
334
|
+
update_app("module A; B = 1; end", 'spec/dir/subdir/app_mod.rb')
|
335
|
+
update_app("class App; A = ::A; def self.call(env) A::B end; end")
|
336
|
+
ru.require 'spec/dir/subdir'
|
337
|
+
ru.require 'spec/app.rb'
|
338
|
+
ru.record_dependency 'spec/dir/subdir', 'spec/app.rb'
|
339
|
+
ru.call({}).should == 1
|
340
|
+
update_app("module A; B = 2; end", 'spec/dir/subdir/app_mod.rb')
|
341
|
+
ru.call({}).should == 2
|
342
|
+
update_app("module A; include C; end", 'spec/dir/subdir/app_mod.rb')
|
343
|
+
update_app("module C; B = 3; end", 'spec/dir/subdir2/app_mod2.rb')
|
344
|
+
ru.require 'spec/dir/subdir2/app_mod2.rb'
|
345
|
+
ru.record_dependency 'spec/dir/subdir2/app_mod2.rb', 'spec/dir/subdir'
|
346
|
+
ru.call({}).should == 3
|
347
|
+
update_app("module C; B = 4; end", 'spec/dir/subdir2/app_mod2.rb')
|
348
|
+
ru.call({}).should == 4
|
349
|
+
end
|
350
|
+
|
351
|
+
it "should handle recorded dependencies in directories when files are added or removed later" do
|
352
|
+
base_ru
|
353
|
+
update_app("class App; A = defined?(::A) ? ::A : Module.new{self::B = 0}; def self.call(env) A::B end; end")
|
354
|
+
ru.record_dependency 'spec/dir/subdir', 'spec/app.rb'
|
355
|
+
ru.record_dependency 'spec/dir/subdir2', 'spec/dir/subdir'
|
356
|
+
ru.require 'spec/app.rb'
|
357
|
+
ru.require 'spec/dir/subdir'
|
358
|
+
ru.require 'spec/dir/subdir2'
|
359
|
+
ru.call({}).should == 0
|
360
|
+
update_app("module A; B = 1; end", 'spec/dir/subdir/app_mod.rb')
|
361
|
+
ru.call({}).should == 1
|
362
|
+
update_app("module A; B = 2; end", 'spec/dir/subdir/app_mod.rb')
|
363
|
+
ru.call({}).should == 2
|
364
|
+
update_app("module C; B = 3; end", 'spec/dir/subdir2/app_mod2.rb')
|
365
|
+
ru.call({}).should == 2
|
366
|
+
update_app("module A; include C; end", 'spec/dir/subdir/app_mod.rb')
|
367
|
+
ru.call({}).should == 3
|
368
|
+
update_app("module C; B = 4; end", 'spec/dir/subdir2/app_mod2.rb')
|
369
|
+
ru.call({}).should == 4
|
370
|
+
File.delete 'spec/dir/subdir/app_mod.rb'
|
371
|
+
ru.call({}).should == 0
|
372
|
+
end
|
373
|
+
|
374
|
+
it "should handle classes split into multiple files" do
|
375
|
+
base_ru
|
376
|
+
update_app("class App; RU.require('spec/dir'); def self.call(env) \"\#{a if respond_to?(:a)}\#{b if respond_to?(:b)}1\".to_i end; end")
|
377
|
+
ru.require 'spec/app.rb'
|
378
|
+
ru.record_split_class 'spec/app.rb', 'spec/dir'
|
379
|
+
ru.call({}).should == 1
|
380
|
+
update_app("class App; def self.a; 2 end end", 'spec/dir/appa.rb')
|
381
|
+
ru.call({}).should == 21
|
382
|
+
update_app("class App; def self.a; 3 end end", 'spec/dir/appa.rb')
|
383
|
+
ru.call({}).should == 31
|
384
|
+
update_app("class App; def self.b; 4 end end", 'spec/dir/appb.rb')
|
385
|
+
ru.call({}).should == 341
|
386
|
+
update_app("class App; def self.a; 5 end end", 'spec/dir/appa.rb')
|
387
|
+
update_app("class App; def self.b; 6 end end", 'spec/dir/appb.rb')
|
388
|
+
ru.call({}).should == 561
|
389
|
+
update_app("class App; end", 'spec/dir/appa.rb')
|
390
|
+
ru.call({}).should == 61
|
391
|
+
File.delete 'spec/dir/appb.rb'
|
392
|
+
ru.call({}).should == 1
|
393
|
+
end
|
394
|
+
|
281
395
|
it "should pick up changes to files in that directory" do
|
282
396
|
base_ru
|
283
397
|
update_app("class App; @a = {}; def self.call(env=nil) @a end; end; RU.require 'spec/dir'")
|
@@ -294,6 +408,22 @@ describe Rack::Unreloader do
|
|
294
408
|
%r{\ARemoved feature .*/spec/dir/a.rb\z}
|
295
409
|
end
|
296
410
|
|
411
|
+
it "should pick up changes to files in subdirectories" do
|
412
|
+
base_ru
|
413
|
+
update_app("class App; @a = {}; def self.call(env=nil) @a end; end; RU.require 'spec/dir'")
|
414
|
+
update_app("App.call[:foo] = 1", 'spec/dir/subdir/a.rb')
|
415
|
+
@ru.require('spec/app.rb')
|
416
|
+
ru.call({}).should == {:foo=>1}
|
417
|
+
update_app("App.call[:foo] = 2", 'spec/dir/subdir/a.rb')
|
418
|
+
ru.call({}).should == {:foo=>2}
|
419
|
+
log_match %r{\ALoading.*spec/app\.rb\z},
|
420
|
+
%r{\ALoading.*spec/dir/subdir/a\.rb\z},
|
421
|
+
%r{\ANew classes in .*spec/app\.rb: App\z},
|
422
|
+
%r{\ANew features in .*spec/app\.rb: .*spec/dir/subdir/a\.rb\z},
|
423
|
+
%r{\AReloading .*/spec/dir/subdir/a.rb\z},
|
424
|
+
%r{\ARemoved feature .*/spec/dir/subdir/a.rb\z}
|
425
|
+
end
|
426
|
+
|
297
427
|
it "should pick up new files added to the directory" do
|
298
428
|
base_ru
|
299
429
|
update_app("class App; @a = {}; def self.call(env=nil) @a end; end; RU.require 'spec/dir'")
|
@@ -306,6 +436,18 @@ describe Rack::Unreloader do
|
|
306
436
|
%r{\ALoading.*spec/dir/a\.rb\z}
|
307
437
|
end
|
308
438
|
|
439
|
+
it "should pick up new files added to subdirectories" do
|
440
|
+
base_ru
|
441
|
+
update_app("class App; @a = {}; def self.call(env=nil) @a end; end; RU.require 'spec/dir'")
|
442
|
+
@ru.require('spec/app.rb')
|
443
|
+
ru.call({}).should == {}
|
444
|
+
update_app("App.call[:foo] = 2", 'spec/dir/subdir/a.rb')
|
445
|
+
ru.call({}).should == {:foo=>2}
|
446
|
+
log_match %r{\ALoading.*spec/app\.rb\z},
|
447
|
+
%r{\ANew classes in .*spec/app\.rb: App\z},
|
448
|
+
%r{\ALoading.*spec/dir/subdir/a\.rb\z}
|
449
|
+
end
|
450
|
+
|
309
451
|
it "should drop files deleted from the directory" do
|
310
452
|
base_ru
|
311
453
|
update_app("class App; @a = {}; def self.call(env=nil) @a end; end; RU.require 'spec/dir'")
|
@@ -322,5 +464,22 @@ describe Rack::Unreloader do
|
|
322
464
|
%r{\ARemoved feature .*/spec/dir/a.rb\z},
|
323
465
|
%r{\ALoading.*spec/dir/b\.rb\z}
|
324
466
|
end
|
467
|
+
|
468
|
+
it "should drop files deleted from subdirectories" do
|
469
|
+
base_ru
|
470
|
+
update_app("class App; @a = {}; def self.call(env=nil) @a end; end; RU.require 'spec/dir'")
|
471
|
+
update_app("App.call[:foo] = 1", 'spec/dir/subdir/a.rb')
|
472
|
+
@ru.require('spec/app.rb')
|
473
|
+
ru.call({}).should == {:foo=>1}
|
474
|
+
File.delete('spec/dir/subdir/a.rb')
|
475
|
+
update_app("App.call[:foo] = 2", 'spec/dir/subdir/b.rb')
|
476
|
+
ru.call({}).should == {:foo=>2}
|
477
|
+
log_match %r{\ALoading.*spec/app\.rb\z},
|
478
|
+
%r{\ALoading.*spec/dir/subdir/a\.rb\z},
|
479
|
+
%r{\ANew classes in .*spec/app\.rb: App\z},
|
480
|
+
%r{\ANew features in .*spec/app\.rb: .*spec/dir/subdir/a\.rb\z},
|
481
|
+
%r{\ARemoved feature .*/spec/dir/subdir/a.rb\z},
|
482
|
+
%r{\ALoading.*spec/dir/subdir/b\.rb\z}
|
483
|
+
end
|
325
484
|
end
|
326
485
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-unreloader
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Evans
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-01-24 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: |
|
14
14
|
Rack::Unreloader is a rack middleware that reloads application files when it
|
@@ -26,6 +26,7 @@ files:
|
|
26
26
|
- README.rdoc
|
27
27
|
- Rakefile
|
28
28
|
- lib/rack/unreloader.rb
|
29
|
+
- lib/rack/unreloader/reloader.rb
|
29
30
|
- spec/unreloader_spec.rb
|
30
31
|
homepage: http://gihub.com/jeremyevans/rack-unreloader
|
31
32
|
licenses:
|
@@ -54,7 +55,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
54
55
|
version: '0'
|
55
56
|
requirements: []
|
56
57
|
rubyforge_project:
|
57
|
-
rubygems_version: 2.
|
58
|
+
rubygems_version: 2.4.5
|
58
59
|
signing_key:
|
59
60
|
specification_version: 4
|
60
61
|
summary: Reload application when files change, unloading constants first
|