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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8eacfdd6cd0d57685d8835bcfbc35bb716cc0241
4
- data.tar.gz: eed0b264873367b89e234d9b74f1b47805ce48f3
3
+ metadata.gz: 37c98d1ba331382609dbc5b74058d02f6f194bc2
4
+ data.tar.gz: 77ae22b4fd6bd6a55f3ceb44d21e632d1fc87a77
5
5
  SHA512:
6
- metadata.gz: 61c47de205a000b7048f516d3c841cc4dc20c88b565be214330e868a3c47a7e2561f90d4bcb7a9ed23eb10d62133639ae0e437e23b528a2f4ff362c8fb179def
7
- data.tar.gz: 0eb1923fb9131e0a84b4ad020b54538bbd69ae7665bcf6f3bd5dc328685491a67aaec2c522baa01a00e1a4669bbebe0f803d65bc00f8f9eea31b1cd38e40fa40
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
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014 Jeremy Evans
1
+ Copyright (c) 2014-2015 Jeremy Evans
2
2
  Copyright (c) 2011 Padrino
3
3
 
4
4
  Permission is hereby granted, free of charge, to any person obtaining
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 run this in development mode.
103
- Assuming you use +RACK_ENV+ to determine development mode, you can change
104
- +config.ru+ to:
105
-
106
- if ENV['RACK_ENV'] == 'development'
107
- require 'rack/unreloader'
108
- Unreloader = Rack::Unreloader.new{App}
109
- Unreloader.require './models.rb'
110
- Unreloader.require './app.rb'
111
- run Unreloader
112
- else
113
- require './app.rb'
114
- run App
115
- end
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
- If there are dependencies that you don't want to require directly in your
118
- +config.ru+, but you do want to use <tt>Rack::Unreloader</tt> for them in
119
- development, you can do:
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
- (defined?(Unreloader) ? Unreloader : Kernel).require './models.rb'
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
- Note that if the modules defined are included in any classes, this doesn't
132
- uninclude them (ruby doesn't support that), so there won't be a change to
133
- that class until that class is reloaded as well.
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 general only a single file will be changed at a time, but it can
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
@@ -1,309 +1,50 @@
1
- require 'set'
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
- class Reloader
8
- # Reference to ::File as File would return Rack::File by default.
9
- F = ::File
10
-
11
- # Regexp for valid constant names, to prevent code execution.
12
- VALID_CONSTANT_NAME_REGEXP = /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/.freeze
13
-
14
- # Setup the reloader. Supports :logger and :subclasses options, see
15
- # Rack::Unloader.new for details.
16
- def initialize(opts={})
17
- @logger = opts[:logger]
18
- @classes = opts[:subclasses] ? Array(opts[:subclasses]).map{|s| s.to_s} : %w'Object'
19
-
20
- # Hash of files being monitored for changes, keyed by absolute path of file name,
21
- # with values being the last modified time (or nil if the file has not yet been loaded).
22
- @monitor_files = {}
23
-
24
- # Hash 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
- # Returns true if the file is new or it's modification time changed.
288
- def file_changed?(file, time = @monitor_files[file])
289
- !time || modified_at(file) > time
290
- end
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
- # Return the time the file was modified at. This can be overridden
293
- # to base the reloading on something other than the file's modification
294
- # time.
295
- def modified_at(file)
296
- F.mtime(file)
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
- @cooldown = opts[:cooldown] || 1
315
- @last = Time.at(0)
316
- @reloader = Reloader.new(opts)
317
- @reloader.reload!
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{@reloader.reload!} : @reloader.reload!
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(depends, &block)
332
- @reloader.require_dependencies(depends, &block)
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
@@ -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 'spec/app.rb'
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/*.rb'].each{|f| File.delete(f)}
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.1.0
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: 2014-09-25 00:00:00.000000000 Z
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.2.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