rack-unreloader 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|