padrino-core 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 44a11b5c0a503449fdce5f86ea6ceff3c6fe7f33e7d28223983b8507c1975aca
4
- data.tar.gz: b3dcecde6843769f9b74336e1b94d1ecbd5cd2d2ae607d5982c2380be1452085
3
+ metadata.gz: 60c8ed53139a3e9558b1e665205eeb798960a6049ac0788e3454f8f6f4280de0
4
+ data.tar.gz: '08dc99355571157c9113a0083c8acacd9970c7f64e407008265fdc177e590572'
5
5
  SHA512:
6
- metadata.gz: dbad97028d154c692aa99e3f9d7b68301c69da4843e905cbafbd59bd2b24026186efe0eaadcdb0df0bc2aff14d803952d507efa5153988d5ede8b29a7d2de60e
7
- data.tar.gz: f9dca7338b15c2276b2546afc826f9e4c39eb2ef8f226dc90fa2666d4ce3b3a2814230aea1f23f4e99985ae45f76bd8c5068e4c5918f8c7aa5fb5b11522aa3d5
6
+ metadata.gz: 50d10641e73dd6b858b2a6490cdc77372055ace5232683d7727e44b45a85267d8bd56fa2c5ce49852c51e5c017169a7449ecb0d297b6b5d67a6896b8f7396b93
7
+ data.tar.gz: 79af9aed08ab7c2189d162b75772e9d40f37ca45189abf26d18c9c42f0ca62d149d30756c77088fdaa119683192c76c94b8e9acf07b6b2abb8d0f72cd214155e
@@ -60,7 +60,10 @@ module Padrino
60
60
  set :views, proc { File.join(root, 'views') }
61
61
 
62
62
  set :uri_root, '/'
63
- set :public_folder, proc { Padrino.root('public', uri_root) }
63
+ set :public_folder, proc {
64
+ app_public = Padrino.root('public', uri_root)
65
+ File.exist?(app_public) ? app_public : Padrino.root('public')
66
+ }
64
67
  set :images_path, proc { File.join(public_folder, 'images') }
65
68
  set :base_url, 'http://localhost'
66
69
  end
@@ -111,10 +114,24 @@ module Padrino
111
114
  builder.use Rack::Head
112
115
  register Padrino::Flash
113
116
  setup_protection builder
117
+ setup_host_authorization builder if host_authorization_set?
114
118
  setup_csrf_protection builder
115
119
  setup_application!
116
120
  end
117
121
 
122
+ ##
123
+ # Sinatra 4.1+ checks the Host header against its host_authorization
124
+ # setting. Its default permits only local hosts in development, so
125
+ # Padrino applies the setting only when it is set to a Hash on the app,
126
+ # a Padrino application it inherits from or in Padrino.configure_apps,
127
+ # and apps that don't set it keep answering any host.
128
+ #
129
+ def host_authorization_set?
130
+ respond_to?(:setup_host_authorization, true) &&
131
+ method(:host_authorization).owner != Sinatra::Base.singleton_class &&
132
+ host_authorization.is_a?(Hash)
133
+ end
134
+
118
135
  ##
119
136
  # We need to add almost __sinatra__ images.
120
137
  #
@@ -381,6 +381,20 @@ module Padrino
381
381
  end
382
382
  end
383
383
 
384
+ ##
385
+ # The project's shared public/ for an app mounted under a sub-path whose
386
+ # public folder is the default public/<uri_root>. Static files and asset
387
+ # stamps fall back to it for files public/<uri_root> doesn't have. Nil for
388
+ # standalone Sinatra apps that only register Padrino::Routing.
389
+ #
390
+ # @api private
391
+ def shared_public_folder
392
+ return unless respond_to?(:uri_root) && Padrino.respond_to?(:root)
393
+ return if uri_root.nil? || uri_root == '/' || !public_folder
394
+
395
+ Padrino.root('public') if File.expand_path(public_folder) == Padrino.root('public', uri_root)
396
+ end
397
+
384
398
  ##
385
399
  # Processes the existing path and prepends the 'parent' parameters onto the route
386
400
  # Used for calculating path in route method.
@@ -857,17 +871,20 @@ module Padrino
857
871
 
858
872
  ##
859
873
  # This is mostly just a helper so request.path_info isn't changed when
860
- # serving files from the public directory.
874
+ # serving files from the public directory. An app mounted under a sub-path
875
+ # falls back to the shared public/ for files missing from public/<uri_root>.
861
876
  #
862
877
  def static_file?(path_info)
863
878
  return unless (public_dir = settings.public_folder)
864
- public_dir = File.expand_path(public_dir)
865
- path = "#{public_dir}#{unescape(path_info)}"
866
- return unless Rack::Utils.valid_path?(path)
867
- path = File.expand_path(path)
868
- return unless path.start_with?("#{public_dir}/")
869
- return unless File.file?(path)
870
- path
879
+ # Decode the path the way Sinatra's static! and the router do: %XX only, so '+' stays a plus.
880
+ file = Rack::Utils.unescape_path(path_info.b).force_encoding(Encoding::UTF_8)
881
+ [File.expand_path(public_dir), settings.shared_public_folder].compact.each do |dir|
882
+ path = "#{dir}#{file}"
883
+ next unless Rack::Utils.valid_path?(path)
884
+ path = File.expand_path(path)
885
+ return path if path.start_with?("#{dir}/") && File.file?(path)
886
+ end
887
+ nil
871
888
  end
872
889
 
873
890
  #
@@ -119,15 +119,22 @@ module Padrino
119
119
  end
120
120
 
121
121
  ##
122
- # Returns default list of path globs to load as dependencies.
123
- # Appends custom dependency patterns to the be loaded for your Application.
122
+ # Returns the files to load as dependencies of your Application: those matching
123
+ # the default globs below, under the application's root. The list is built again
124
+ # on each call, so appending to it has no effect; to load more files, override
125
+ # the method and add them to +super+. Add file paths (expand globs with Dir.glob)
126
+ # under Padrino.root: the reloader watches these files and reloads the application
127
+ # when one changes.
124
128
  #
125
- # @return [Array]
126
- # list of path globs to load as dependencies
129
+ # @return [Array<String>]
130
+ # paths of the files to load as dependencies
127
131
  #
128
132
  # @example
129
- # MyApp.dependencies << "#{Padrino.root}/uploaders/**/*.rb"
130
- # MyApp.dependencies << Padrino.root('other_app', 'controllers.rb')
133
+ # class MyApp < Padrino::Application
134
+ # def self.dependencies
135
+ # super + Dir.glob(Padrino.root('uploaders', '**', '*.rb'))
136
+ # end
137
+ # end
131
138
  #
132
139
  def dependencies
133
140
  [
@@ -21,7 +21,8 @@ module Padrino
21
21
  if File.file?(binstub)
22
22
  exec Gem.ruby, binstub, *ARGV
23
23
  else
24
- puts 'Please run `bundle install && bundle binstubs --all` from your project root to generate bundle-specific executables'
24
+ # exit! skips the flush of buffered output, so write to the unbuffered stderr. Not warn: -W0 silences it.
25
+ $stderr.puts 'Please run `bundle install && bundle binstubs --all` from your project root to generate bundle-specific executables'
25
26
  exit!
26
27
  end
27
28
  end
@@ -34,8 +34,9 @@ module Padrino
34
34
  end
35
35
 
36
36
  ##
37
- # Returns the list of path globs to load as dependencies.
38
- # Appends custom dependency patterns to the be loaded for Padrino.
37
+ # Returns the path globs of the module's files to load as dependencies. The list
38
+ # is built again on each call, so appending to it has no effect; to load more
39
+ # files, override the method and add globs to +super+.
39
40
  #
40
41
  # @return [Array<String>]
41
42
  # The dependency paths.
@@ -44,9 +45,11 @@ module Padrino
44
45
  # module MyModule
45
46
  # extend Padrino::Module
46
47
  # gem! 'my_gem'
47
- # end
48
48
  #
49
- # Module.dependency_paths << "#{MyModule.root}/uploaders/*.rb"
49
+ # def self.dependency_paths
50
+ # super + ["#{root}/uploaders/*.rb"]
51
+ # end
52
+ # end
50
53
  #
51
54
  def dependency_paths
52
55
  [
@@ -70,6 +70,8 @@ module Padrino
70
70
  # Returns a instance of PathRouter::Route if path is matched with current regexp
71
71
  #
72
72
  def match?(offset, path)
73
+ return unless path
74
+
73
75
  @routes[offset..].find do |route|
74
76
  route.regexp === path || (path.end_with?('/') && route.regexp === path[0..-2])
75
77
  end
@@ -88,10 +90,14 @@ module Padrino
88
90
  end
89
91
 
90
92
  ##
91
- # Decode env["PATH_INFO"]
93
+ # Decode env["PATH_INFO"]. Returns nil when the path doesn't decode to valid text
94
+ # (a malformed or non-UTF-8 escape such as "/%ff"), so it matches no route.
92
95
  #
93
96
  def decode_pattern(pattern)
94
- decode_uri(encode_default_external(pattern))
97
+ decoded = decode_uri(encode_default_external(pattern))
98
+ decoded if decoded.valid_encoding?
99
+ rescue ArgumentError, EncodingError
100
+ nil
95
101
  end
96
102
 
97
103
  ##
@@ -20,6 +20,7 @@ module Padrino
20
20
 
21
21
  def prepare(name)
22
22
  file = remove(name)
23
+ reset_concerns(name)
23
24
  @old_entries ||= {}
24
25
  @old_entries[name] = {
25
26
  constants: object_classes,
@@ -57,6 +58,24 @@ module Padrino
57
58
  @files ||= {}
58
59
  end
59
60
 
61
+ ##
62
+ # Resets the ActiveSupport::Concern modules first defined in the file. Unlike classes
63
+ # they are kept on reload, so that classes including them keep working and namespaces
64
+ # keep their constants, but their state is reset so the file can define them again.
65
+ #
66
+ def reset_concerns(file)
67
+ return unless defined?(::ActiveSupport::Concern)
68
+
69
+ ObjectSpace.each_object(::Module) do |mod|
70
+ # Module#=== doesn't call mod, which can be a DeprecatedConstantProxy that warns on any call.
71
+ next unless ::ActiveSupport::Concern === mod && mod.name
72
+ next unless Object.const_source_location(mod.name)&.first == file
73
+ Reloader.reset_concern(mod) if Object.const_get(mod.name).equal?(mod)
74
+ rescue NameError
75
+ next
76
+ end
77
+ end
78
+
60
79
  ##
61
80
  # Returns all the classes in the object space.
62
81
  #
@@ -153,6 +153,18 @@ module Padrino
153
153
  # no const to remove
154
154
  end
155
155
 
156
+ ##
157
+ # Clears the included and prepended blocks of an ActiveSupport::Concern, so that its file
158
+ # can define them again. Its dependencies are reset by +extend ActiveSupport::Concern+.
159
+ #
160
+ def reset_concern(concern)
161
+ return if constant_excluded?(concern)
162
+ %i[@_included_block @_prepended_block].each do |ivar|
163
+ concern.remove_instance_variable(ivar) if concern.instance_variable_defined?(ivar)
164
+ end
165
+ logger.devel "Reset concern #{concern}"
166
+ end
167
+
156
168
  ##
157
169
  # Remove a feature from $LOADED_FEATURES so it can be required again.
158
170
  #
@@ -55,6 +55,8 @@ module Padrino
55
55
  %i[INT TERM].each { |sig| trap(sig) { exit } }
56
56
  super do |server|
57
57
  server.threaded = true if server.respond_to?(:threaded=)
58
+ # WEBrick: stop on TERM the way Rackup stops it on INT, instead of raising SystemExit in its accept loop.
59
+ trap(:TERM) { server.shutdown } if server.respond_to?(:shutdown)
58
60
  end
59
61
  ensure
60
62
  puts '<= Padrino leaves the gun, takes the cannoli' unless options[:daemonize]
@@ -6,7 +6,7 @@
6
6
  #
7
7
  module Padrino
8
8
  # The version constant for the current version of Padrino.
9
- VERSION = '0.17.0' unless defined?(Padrino::VERSION)
9
+ VERSION = '0.18.0' unless defined?(Padrino::VERSION)
10
10
 
11
11
  #
12
12
  # The current Padrino version.
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'mixins/sortable'
4
+
5
+ class CatalogItem
6
+ include Sortable
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FilteredItem
4
+ include Filterable
5
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Filterable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ define_method(:filter) { 'v1' }
8
+ end
9
+
10
+ def label
11
+ 'l1'
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sortable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ define_method(:sort_key) { 's1' }
8
+ end
9
+
10
+ def sort_label
11
+ 's1'
12
+ end
13
+ end
@@ -1,5 +1,5 @@
1
1
  begin
2
- require '/tmp/padrino_class_demo'
2
+ require 'padrino_class_demo'
3
3
  rescue LoadError
4
4
  # do nothing
5
5
  end
@@ -1,5 +1,5 @@
1
1
  begin
2
- require '/tmp/padrino_instance_demo'
2
+ require 'padrino_instance_demo'
3
3
  rescue LoadError
4
4
  # do nothing
5
5
  end
data/test/test_binstub.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require_relative 'helper'
2
+ require 'open3'
2
3
  require 'tmpdir'
3
4
  require 'padrino-core/cli/binstub'
4
5
 
@@ -7,15 +8,40 @@ describe 'Padrino.replace_with_binstub' do
7
8
  Dir.mktmpdir do |dir|
8
9
  File.write(File.join(dir, 'Gemfile'), '')
9
10
  File.write(File.join(dir, '.components'), '')
10
- out, = capture_io do
11
+ _, err = capture_io do
11
12
  Bundler.stub(:definition, -> { raise Bundler::GemfileNotFound }) do
12
13
  Padrino.stub(:exit!, nil) do
13
14
  Dir.chdir(dir) { Padrino.replace_with_binstub('padrino') }
14
15
  end
15
16
  end
16
17
  end
17
- assert_match(/bundle install && bundle binstubs --all/, out)
18
- refute_match(/--binstubs/, out)
18
+ assert_match(/bundle install && bundle binstubs --all/, err)
19
+ refute_match(/--binstubs/, err)
19
20
  end
20
21
  end
22
+
23
+ # Runs the executable outside any bundle in a project without bin/, with its output piped.
24
+ def run_without_binstubs(bin, env = {})
25
+ Dir.mktmpdir do |dir|
26
+ File.write(File.join(dir, 'Gemfile'), '')
27
+ File.write(File.join(dir, '.components'), '')
28
+ run = -> { Open3.capture3(env, RbConfig.ruby, '-I', File.expand_path('../lib', __dir__), bin, 'version', chdir: dir) }
29
+ defined?(Bundler) ? Bundler.with_unbundled_env(&run) : run.call
30
+ end
31
+ end
32
+
33
+ %w[padrino-core/bin/padrino padrino-gen/bin/padrino-gen].each do |executable|
34
+ it "should show the binstubs hint and exit 1 when the output of #{executable} is piped" do
35
+ out, err, status = run_without_binstubs(File.expand_path("../../#{executable}", __dir__))
36
+ assert_equal 1, status.exitstatus
37
+ assert_includes err, 'bundle install && bundle binstubs --all'
38
+ refute_includes out, 'bundle binstubs'
39
+ end
40
+ end
41
+
42
+ it 'should show the binstubs hint with warnings turned off' do
43
+ _, err, status = run_without_binstubs(File.expand_path('../../padrino-core/bin/padrino', __dir__), 'RUBYOPT' => '-W0')
44
+ assert_equal 1, status.exitstatus
45
+ assert_includes err, 'bundle install && bundle binstubs --all'
46
+ end
21
47
  end
@@ -0,0 +1,117 @@
1
+ require_relative 'helper'
2
+
3
+ describe 'Host authorization' do
4
+ before do
5
+ skip 'needs Sinatra 4.1+' unless Sinatra::Base.respond_to?(:host_authorization)
6
+ Padrino.clear!
7
+ end
8
+
9
+ def status_for(rack_app, host, path = '/')
10
+ Rack::MockRequest.new(rack_app).get(path, 'HTTP_HOST' => host).status
11
+ end
12
+
13
+ describe 'when the app sets host_authorization' do
14
+ it 'should answer permitted hosts and refuse others' do
15
+ mock_app do
16
+ set :host_authorization, permitted_hosts: ['good.example']
17
+ get('/') { 'ok' }
18
+ end
19
+ assert_equal 200, status_for(@app, 'good.example')
20
+ assert_equal 403, status_for(@app, 'evil.example')
21
+ end
22
+
23
+ it 'should apply it in development' do
24
+ mock_app do
25
+ set :environment, :development
26
+ disable :reload
27
+ set :host_authorization, permitted_hosts: ['good.example']
28
+ get('/') { 'ok' }
29
+ end
30
+ assert_equal 200, status_for(@app, 'good.example')
31
+ assert_equal 403, status_for(@app, 'evil.example')
32
+ end
33
+
34
+ it 'should apply a setting inherited from a parent app class' do
35
+ parent = Sinatra.new(Padrino::Application) do
36
+ set :host_authorization, permitted_hosts: ['good.example']
37
+ end
38
+ mock_app(parent) { get('/') { 'ok' } }
39
+ assert_equal 200, status_for(@app, 'good.example')
40
+ assert_equal 403, status_for(@app, 'evil.example')
41
+ end
42
+
43
+ it 'should apply a setting from Padrino.configure_apps' do
44
+ Padrino.configure_apps { set :host_authorization, permitted_hosts: ['good.example'] }
45
+ mock_app { get('/') { 'ok' } }
46
+ assert_equal 200, status_for(@app, 'good.example')
47
+ assert_equal 403, status_for(@app, 'evil.example')
48
+ ensure
49
+ Padrino.clear!
50
+ end
51
+ end
52
+
53
+ describe 'when the app does not set host_authorization' do
54
+ %i[development production test].each do |env|
55
+ it "should answer any host in #{env}" do
56
+ mock_app do
57
+ set :environment, env
58
+ disable :reload
59
+ get('/') { 'ok' }
60
+ end
61
+ assert_equal 200, status_for(@app, 'myapp.example')
62
+ assert_equal 200, status_for(@app, 'localhost')
63
+ end
64
+ end
65
+
66
+ it 'should treat a setting that is not a Hash as unset' do
67
+ mock_app do
68
+ disable :host_authorization
69
+ get('/') { 'ok' }
70
+ end
71
+ assert_equal 200, status_for(@app, 'myapp.example')
72
+ end
73
+ end
74
+
75
+ describe 'for mounted apps' do
76
+ before do
77
+ Object.send(:remove_const, :HostAuthApp) if defined?(::HostAuthApp)
78
+ ::HostAuthApp = Class.new(Padrino::Application) do
79
+ set :host_authorization, permitted_hosts: ['good.example']
80
+ get('/') { 'ok' }
81
+ end
82
+ end
83
+
84
+ after do
85
+ Object.send(:remove_const, :HostAuthApp)
86
+ Padrino.clear!
87
+ end
88
+
89
+ it 'should apply it to an app mounted at the root' do
90
+ Padrino.mount('HostAuthApp', app_file: __FILE__).to('/')
91
+ assert_equal 200, status_for(Padrino.application, 'good.example')
92
+ assert_equal 403, status_for(Padrino.application, 'evil.example')
93
+ end
94
+
95
+ it 'should apply it to an app mounted under a sub-path' do
96
+ Padrino.mount('HostAuthApp', app_file: __FILE__).to('/sub')
97
+ assert_equal 200, status_for(Padrino.application, 'good.example', '/sub')
98
+ assert_equal 403, status_for(Padrino.application, 'evil.example', '/sub')
99
+ end
100
+
101
+ it 'should apply it to an app mounted for a host pattern' do
102
+ Padrino.mount('HostAuthApp', app_file: __FILE__).host(/\.example\z/)
103
+ assert_equal 200, status_for(Padrino.application, 'good.example')
104
+ assert_equal 403, status_for(Padrino.application, 'evil.example')
105
+ end
106
+
107
+ it 'should leave a sibling app without the setting open to any host' do
108
+ ::HostAuthOpenApp = Class.new(Padrino::Application) { get('/') { 'open' } }
109
+ Padrino.mount('HostAuthApp', app_file: __FILE__).to('/locked')
110
+ Padrino.mount('HostAuthOpenApp', app_file: __FILE__).to('/')
111
+ assert_equal 403, status_for(Padrino.application, 'evil.example', '/locked')
112
+ assert_equal 200, status_for(Padrino.application, 'evil.example', '/')
113
+ ensure
114
+ Object.send(:remove_const, :HostAuthOpenApp) if defined?(::HostAuthOpenApp)
115
+ end
116
+ end
117
+ end
data/test/test_mounter.rb CHANGED
@@ -254,6 +254,93 @@ describe 'Mounter' do
254
254
  assert_equal File.read(__FILE__), res.body
255
255
  end
256
256
 
257
+ describe 'serving static assets under a sub-path' do
258
+ def with_public_files(*files, &block)
259
+ Dir.mktmpdir do |root|
260
+ files.each do |file|
261
+ path = File.join(root, 'public', file)
262
+ FileUtils.mkdir_p(File.dirname(path))
263
+ File.write(path, file)
264
+ end
265
+ Padrino.stub(:root, proc { |*args| File.expand_path(File.join(root, *args)) }, &block)
266
+ end
267
+ end
268
+
269
+ it 'should serve assets from the project public folder' do
270
+ class ::StaticSubApp < Padrino::Application; end
271
+ with_public_files('stylesheets/site.css') do
272
+ Padrino.mount('static_sub_app').to('/myapp')
273
+ res = Rack::MockRequest.new(Padrino.application).get('/myapp/stylesheets/site.css')
274
+ assert res.ok?
275
+ assert_equal 'stylesheets/site.css', res.body
276
+ end
277
+ end
278
+
279
+ it 'should serve assets from a public folder named after the uri root' do
280
+ class ::StaticOwnApp < Padrino::Application; end
281
+ with_public_files('stylesheets/site.css', 'myapp/stylesheets/site.css') do
282
+ Padrino.mount('static_own_app').to('/myapp')
283
+ res = Rack::MockRequest.new(Padrino.application).get('/myapp/stylesheets/site.css')
284
+ assert res.ok?
285
+ assert_equal 'myapp/stylesheets/site.css', res.body
286
+ end
287
+ end
288
+
289
+ it 'should fall back to the project public folder for files missing from the uri root folder' do
290
+ class ::StaticPartialApp < Padrino::Application; end
291
+ with_public_files('stylesheets/site.css', 'myapp/.keep') do
292
+ Padrino.mount('static_partial_app').to('/myapp')
293
+ res = Rack::MockRequest.new(Padrino.application).get('/myapp/stylesheets/site.css')
294
+ assert res.ok?
295
+ assert_equal 'stylesheets/site.css', res.body
296
+ end
297
+ end
298
+
299
+ it 'should keep serving shared assets when the application is built again' do
300
+ class ::StaticRebuiltApp < Padrino::Application; end
301
+ with_public_files('stylesheets/site.css') do
302
+ Padrino.mount('static_rebuilt_app').to('/myapp')
303
+ 3.times do
304
+ res = Rack::MockRequest.new(Padrino.application).get('/myapp/stylesheets/site.css')
305
+ assert res.ok?
306
+ assert_equal 'stylesheets/site.css', res.body
307
+ end
308
+ end
309
+ end
310
+
311
+ it 'should serve assets and routes when the app has a helper named like an internal method' do
312
+ class ::StaticHelperApp < Padrino::Application
313
+ helpers(Module.new { define_method(:public_file) { |name| "/files/#{name}" } })
314
+ get('/page') { public_file('a') }
315
+ end
316
+ with_public_files('stylesheets/site.css') do
317
+ Padrino.mount('static_helper_app').to('/myapp')
318
+ assert_equal 'stylesheets/site.css', Rack::MockRequest.new(Padrino.application).get('/myapp/stylesheets/site.css').body
319
+ assert_equal '/files/a', Rack::MockRequest.new(Padrino.application).get('/myapp/page').body
320
+ end
321
+ end
322
+
323
+ it 'should not fall back from a custom public folder' do
324
+ class ::StaticCustomApp < Padrino::Application; end
325
+ with_public_files('stylesheets/site.css', 'custom/.keep') do
326
+ StaticCustomApp.set :public_folder, Padrino.root('public', 'custom')
327
+ Padrino.mount('static_custom_app').to('/myapp')
328
+ res = Rack::MockRequest.new(Padrino.application).get('/myapp/stylesheets/site.css')
329
+ assert_equal 404, res.status
330
+ end
331
+ end
332
+
333
+ it 'should serve assets for an app mounted at the root' do
334
+ class ::StaticRootApp < Padrino::Application; end
335
+ with_public_files('stylesheets/site.css') do
336
+ Padrino.mount('static_root_app').to('/')
337
+ res = Rack::MockRequest.new(Padrino.application).get('/stylesheets/site.css')
338
+ assert res.ok?
339
+ assert_equal 'stylesheets/site.css', res.body
340
+ end
341
+ end
342
+ end
343
+
257
344
  it 'should load apps from gems' do
258
345
  spec_file = Padrino.root('fixtures', 'app_gem', 'app_gem.gemspec')
259
346
  spec = Gem::Specification.load(spec_file)
@@ -30,15 +30,14 @@ describe 'Padrino::Reloader::Storage' do
30
30
  end
31
31
 
32
32
  it 'should return list of new classes' do
33
- skip
34
33
  class OSTest; end
35
34
  module OSTestModule; class B; end; end
36
35
 
37
36
  new_classes = Padrino::Reloader::Storage.send(:new_classes, @snapshot)
38
37
 
39
- assert_equal new_classes.size, 3
40
- assert_equal new_classes.include?(OSTest), true
41
- assert_equal new_classes.include?(OSTestModule::B), true
38
+ assert_includes new_classes, OSTest
39
+ assert_includes new_classes, OSTestModule::B
40
+ refute_includes new_classes, Padrino::Logger
42
41
  end
43
42
 
44
43
  it 'should return a Set object' do
@@ -70,9 +70,10 @@ describe 'SystemReloader' do
70
70
  end
71
71
  end
72
72
  DOC
73
- tmp_file = '/tmp/padrino_class_demo.rb'
73
+ gem_dir = Dir.mktmpdir
74
+ File.write(File.join(gem_dir, 'padrino_class_demo.rb'), new_class)
75
+ $LOAD_PATH.unshift(gem_dir)
74
76
  begin
75
- File.open(tmp_file, 'w') { |f| f.write(new_class) }
76
77
  Padrino.clear!
77
78
  require File.expand_path("#{__dir__}/fixtures/apps/system_class_methods_demo.rb")
78
79
  @app = SystemClassMethodsDemo
@@ -85,7 +86,8 @@ describe 'SystemReloader' do
85
86
  assert defined?(SingletonClassTest), 'SingletonClassTest undefined'
86
87
  assert_includes SingletonClassTest.singleton_methods, :external_test
87
88
  ensure
88
- FileUtils.rm tmp_file
89
+ $LOAD_PATH.delete(gem_dir)
90
+ FileUtils.rm_rf(gem_dir)
89
91
  end
90
92
  end
91
93
 
@@ -96,9 +98,10 @@ describe 'SystemReloader' do
96
98
  end
97
99
  end
98
100
  DOC
99
- tmp_file = '/tmp/padrino_instance_demo.rb'
101
+ gem_dir = Dir.mktmpdir
102
+ File.write(File.join(gem_dir, 'padrino_instance_demo.rb'), new_class)
103
+ $LOAD_PATH.unshift(gem_dir)
100
104
  begin
101
- File.open(tmp_file, 'w') { |f| f.write(new_class) }
102
105
  Padrino.clear!
103
106
  require File.expand_path("#{__dir__}/fixtures/apps/system_instance_methods_demo.rb")
104
107
  @app = SystemInstanceMethodsDemo
@@ -111,7 +114,8 @@ describe 'SystemReloader' do
111
114
  assert defined?(InstanceTest), 'InstanceTest undefined'
112
115
  assert_includes InstanceTest.new.methods, :instance_test
113
116
  ensure
114
- FileUtils.rm tmp_file
117
+ $LOAD_PATH.delete(gem_dir)
118
+ FileUtils.rm_rf(gem_dir)
115
119
  end
116
120
  end
117
121
 
@@ -187,5 +191,94 @@ describe 'SystemReloader' do
187
191
  Padrino.reload!
188
192
  assert_equal original_value, BadModule.instance_variable_get(:@happy_global_variable)
189
193
  end
194
+
195
+ it 'should redefine an ActiveSupport::Concern whose included block moved' do
196
+ begin
197
+ require 'active_support/concern'
198
+ rescue LoadError
199
+ skip 'ActiveSupport is not available'
200
+ end
201
+ concern_file = "#{__dir__}/fixtures/apps/concerned/models/mixins/filterable.rb"
202
+ includer_file = "#{__dir__}/fixtures/apps/concerned/models/filtered_item.rb"
203
+ original = File.read(concern_file)
204
+ Padrino.clear!
205
+ require File.expand_path("#{__dir__}/fixtures/apps/concerned/app.rb")
206
+ @app = SystemConcernedClassDemo
207
+ Padrino.mount(SystemConcernedClassDemo).to('/')
208
+ get '/'
209
+ assert_equal 'v1', FilteredItem.new.filter
210
+ Filterable.instance_variable_set(:@config, :kept)
211
+
212
+ # A second edit checks that the concern stays tracked after its first reload.
213
+ [2, 3].each do |version|
214
+ File.write(concern_file, original.sub(' included do', "#{" # moved\n" * version} included do")
215
+ .sub("'v1'", "'v#{version}'").sub("'l1'", "'l#{version}'"))
216
+ FileUtils.touch(concern_file, mtime: Time.now + version)
217
+ Padrino.reload!
218
+ assert_equal "l#{version}", FilteredItem.new.label
219
+ FileUtils.touch(includer_file, mtime: Time.now + version)
220
+ Padrino.reload!
221
+ assert_equal "v#{version}", FilteredItem.new.filter
222
+ end
223
+ assert_equal :kept, Filterable.instance_variable_get(:@config)
224
+ ensure
225
+ File.write(concern_file, original) if original
226
+ end
227
+
228
+ it 'should redefine an ActiveSupport::Concern that another file requires, also after Padrino.clear!' do
229
+ begin
230
+ require 'active_support/concern'
231
+ rescue LoadError
232
+ skip 'ActiveSupport is not available'
233
+ end
234
+ concern_file = "#{__dir__}/fixtures/apps/concerned/models/mixins/sortable.rb"
235
+ includer_file = "#{__dir__}/fixtures/apps/concerned/models/catalog_item.rb"
236
+ original = File.read(concern_file)
237
+ # Sortable stays defined after Padrino.clear!, so the second load finds it already defined.
238
+ 2.times do
239
+ Padrino.clear!
240
+ require File.expand_path("#{__dir__}/fixtures/apps/concerned/app.rb")
241
+ @app = SystemConcernedClassDemo
242
+ Padrino.mount(SystemConcernedClassDemo).to('/')
243
+ get '/'
244
+ end
245
+ assert_equal 's1', CatalogItem.new.sort_key
246
+
247
+ [2, 3].each do |version|
248
+ File.write(concern_file, original.sub(' included do', "#{" # moved\n" * version} included do").gsub("'s1'", "'s#{version}'"))
249
+ FileUtils.touch(concern_file, mtime: Time.now + version)
250
+ Padrino.reload!
251
+ assert_equal "s#{version}", CatalogItem.new.sort_label
252
+ FileUtils.touch(includer_file, mtime: Time.now + version)
253
+ Padrino.reload!
254
+ assert_equal "s#{version}", CatalogItem.new.sort_key
255
+ end
256
+ ensure
257
+ if original
258
+ File.write(concern_file, original)
259
+ # The next Padrino.clear! keeps Sortable, and CatalogItem then requires the restored file without a reset.
260
+ Padrino::Reloader.reset_concern(Sortable) if defined?(Sortable)
261
+ end
262
+ end
263
+
264
+ it 'should not call methods on deprecated constant proxies while looking for concerns' do
265
+ begin
266
+ require 'active_support'
267
+ require 'active_support/deprecation'
268
+ rescue LoadError
269
+ skip 'ActiveSupport is not available'
270
+ end
271
+ # A proxy is a Module that warns, or raises with this behavior, on any method call,
272
+ # so the test only keeps a reference to it and never calls it.
273
+ deprecator = ActiveSupport::Deprecation.new
274
+ deprecator.behavior = :raise
275
+ _proxy = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('ReloaderDeprecatedMonitor', '::Monitor', deprecator)
276
+ Padrino.clear!
277
+ require File.expand_path("#{__dir__}/fixtures/apps/concerned/app.rb")
278
+ @app = SystemConcernedClassDemo
279
+ Padrino.mount(SystemConcernedClassDemo).to('/')
280
+ get '/'
281
+ assert_equal 'l1', FilteredItem.new.label
282
+ end
190
283
  end
191
284
  end
data/test/test_routing.rb CHANGED
@@ -89,6 +89,52 @@ describe 'Routing' do
89
89
  end
90
90
  end
91
91
 
92
+ it 'should keep a plus sign in static file paths' do
93
+ Dir.mktmpdir do |tmp|
94
+ dir = "#{tmp}/\u00fcber"
95
+ FileUtils.mkdir_p(["#{dir}/public", "#{dir}/public_bak"])
96
+ File.write("#{dir}/public/a+b.txt", 'plus')
97
+ File.write("#{dir}/public/c d.txt", 'space')
98
+ File.write("#{dir}/public/caf\u00e9.txt", 'cafe')
99
+ File.write("#{dir}/public_bak/secret.txt", 'secret')
100
+ mock_app do
101
+ set :public_folder, "#{dir}/public"
102
+ get('/*') { 'not found' }
103
+ end
104
+ get '/a+b.txt'
105
+ assert_equal 'plus', body
106
+ get '/a%2Bb.txt'
107
+ assert_equal 'plus', body
108
+ get '/c%20d.txt'
109
+ assert_equal 'space', body
110
+ get '/c+d.txt'
111
+ assert_equal 'not found', body
112
+ get '/caf%C3%A9.txt'
113
+ assert_equal 'cafe', body
114
+ get '/%2e%2e/public_bak/secret.txt'
115
+ assert_equal 'not found', body
116
+ get '/a+b.txt%00.png'
117
+ assert_equal 'not found', body
118
+ end
119
+ end
120
+
121
+ it 'should keep a plus sign in static file paths of an app mounted under a sub-path' do
122
+ Dir.mktmpdir do |dir|
123
+ FileUtils.mkdir_p("#{dir}/public")
124
+ File.write("#{dir}/public/a+b.txt", 'plus')
125
+ File.write("#{dir}/public/c d.txt", 'space')
126
+ mock_app do
127
+ set :public_folder, "#{dir}/public"
128
+ get('/*') { 'not found' }
129
+ end
130
+ mounted = Rack::Builder.app(@app) { |inner| map('/blog') { run inner } }
131
+ request = Rack::MockRequest.new(mounted)
132
+ assert_equal 'plus', request.get('/blog/a+b.txt').body
133
+ assert_equal 'not found', request.get('/blog/c+d.txt').body
134
+ assert_equal 'space', request.get('/blog/c%20d.txt').body
135
+ end
136
+ end
137
+
92
138
  it 'should ignore trailing delimiters for basic route' do
93
139
  mock_app do
94
140
  get('/foo') { 'okey' }
@@ -253,6 +299,30 @@ describe 'Routing' do
253
299
  assert_equal 'cpp', body
254
300
  end
255
301
 
302
+ it 'should not find a route for a path that does not decode to valid UTF-8' do
303
+ mock_app do
304
+ get('/') { 'root' }
305
+ get("/caf\u00e9") { 'cafe' }
306
+ get(:show, map: '/items/:id') { params[:id] }
307
+ end
308
+ { '' => Padrino::Router.new(path: '/', to: @app), '/blog' => Padrino::Router.new(path: '/blog', to: @app) }.each do |prefix, app|
309
+ # Set PATH_INFO directly: Rack::MockRequest refuses to parse a malformed escape such as '%zz'.
310
+ request = lambda do |path|
311
+ env = Rack::MockRequest.env_for('/')
312
+ env['PATH_INFO'] = "#{prefix}#{path}"
313
+ status, _headers, body = app.call(env)
314
+ [status, body.to_enum.to_a.join]
315
+ end
316
+ ['/%ff.txt', '/a%80', '/items/%ff', '/items/%c0%ae%c0%ae', '/a%zz', '/items/%'].each do |path|
317
+ assert_equal 404, request.call(path)[0], "#{prefix}#{path}"
318
+ end
319
+ assert_equal [200, 'root'], request.call('/')
320
+ assert_equal [200, 'cafe'], request.call('/caf%C3%A9')
321
+ assert_equal [200, "caf\u00e9"], request.call('/items/caf%C3%A9')
322
+ assert_equal [200, 'a+b'], request.call('/items/a+b')
323
+ end
324
+ end
325
+
256
326
  it 'should keep path params UTF-8 regardless of default_external' do
257
327
  mock_app { get(:show, map: '/items/:id') { params[:id] == 'щ' ? 'ok' : "bad #{params[:id].encoding}" } }
258
328
  old_verbose = $VERBOSE
@@ -1174,6 +1244,17 @@ describe 'Routing' do
1174
1244
  assert_equal 'show 3 1 2', body
1175
1245
  end
1176
1246
 
1247
+ it 'should name the parent param after a parent that ends in s' do
1248
+ mock_app do
1249
+ controller :comments, parent: :status do
1250
+ get(:index) { "index #{params[:status_id]}" }
1251
+ end
1252
+ end
1253
+ get '/status/1/comments'
1254
+ assert_equal 'index 1', body
1255
+ assert_equal '/status/1/comments', @app.url(:comments, :index, status_id: 1)
1256
+ end
1257
+
1177
1258
  it 'should respect parent precedence: controllers parents go before route parents' do
1178
1259
  mock_app do
1179
1260
  controllers :project do
@@ -2501,4 +2582,21 @@ describe 'Routing' do
2501
2582
  get '/'
2502
2583
  assert_equal 200, status
2503
2584
  end
2585
+
2586
+ it 'functions in a standalone app that has a public folder' do
2587
+ Dir.mktmpdir do |root|
2588
+ FileUtils.mkdir_p(File.join(root, 'public'))
2589
+ File.write(File.join(root, 'public', 'a.txt'), 'static')
2590
+ mock_app(Sinatra::Application) do
2591
+ set :root, root
2592
+ register Padrino::Routing
2593
+ get(:index) { 'Standalone' }
2594
+ end
2595
+ get '/'
2596
+ assert_equal 200, status
2597
+ assert_equal 'Standalone', body
2598
+ get '/a.txt'
2599
+ assert_equal 'static', body
2600
+ end
2601
+ end
2504
2602
  end
data/test/test_server.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  require_relative 'helper'
2
+ require 'open3'
3
+ require 'timeout'
2
4
 
3
5
  describe 'Padrino::Server' do
4
6
  describe 'detect_rack_handler' do
@@ -29,4 +31,60 @@ describe 'Padrino::Server' do
29
31
  end
30
32
  end
31
33
  end
34
+
35
+ describe 'signals' do
36
+ # Starts Padrino::Server in a child process on a fake handler that, like WEBrick, yields its
37
+ # server and then loops until the server is shut down, sends the child TERM and returns its output.
38
+ def start_and_term(server_methods)
39
+ script = <<~RUBY
40
+ require 'padrino-core'
41
+ class FakeServer
42
+ #{server_methods}
43
+ def start
44
+ puts 'READY'
45
+ $stdout.flush
46
+ sleep 0.05 until @stopped
47
+ puts 'STOPPED'
48
+ end
49
+ end
50
+ module FakeHandler
51
+ def self.run(_app, **_options)
52
+ server = FakeServer.new
53
+ yield server
54
+ server.start
55
+ end
56
+ end
57
+ Rackup::Handler.register('fake', FakeHandler)
58
+ Padrino::Server.start(->(_env) { [200, {}, []] }, server: 'fake', Port: 0)
59
+ RUBY
60
+ lib = File.expand_path('../lib', __dir__)
61
+ Open3.popen2e({ 'RACK_ENV' => 'test' }, RbConfig.ruby, '-I', lib, '-e', script) do |_stdin, out, wait|
62
+ output = +''
63
+ Timeout.timeout(60) do
64
+ output << out.gets.to_s until output.include?('READY') || out.eof?
65
+ Process.kill('TERM', wait.pid)
66
+ output << out.read
67
+ [output, wait.value]
68
+ end
69
+ rescue Timeout::Error
70
+ Process.kill('KILL', wait.pid)
71
+ raise
72
+ end
73
+ end
74
+
75
+ it 'should shut the server down on TERM when it supports shutdown' do
76
+ output, status = start_and_term("def shutdown\n puts 'SHUTDOWN'\n @stopped = true\nend")
77
+ assert status.success?, output
78
+ assert_includes output, 'SHUTDOWN'
79
+ assert_includes output, 'STOPPED'
80
+ refute_includes output, 'SystemExit'
81
+ end
82
+
83
+ it 'should exit on TERM when the server does not support shutdown' do
84
+ output, status = start_and_term('')
85
+ assert status.success?, output
86
+ refute_includes output, 'STOPPED'
87
+ assert_includes output, 'leaves the gun'
88
+ end
89
+ end
32
90
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: padrino-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Padrino Team
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2026-10-14 00:00:00.000000000 Z
14
+ date: 2026-10-31 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: ostruct
@@ -33,14 +33,14 @@ dependencies:
33
33
  requirements:
34
34
  - - '='
35
35
  - !ruby/object:Gem::Version
36
- version: 0.17.0
36
+ version: 0.18.0
37
37
  type: :runtime
38
38
  prerelease: false
39
39
  version_requirements: !ruby/object:Gem::Requirement
40
40
  requirements:
41
41
  - - '='
42
42
  - !ruby/object:Gem::Version
43
- version: 0.17.0
43
+ version: 0.18.0
44
44
  - !ruby/object:Gem::Dependency
45
45
  name: rackup
46
46
  requirement: !ruby/object:Gem::Requirement
@@ -145,7 +145,11 @@ files:
145
145
  - test/fixtures/app_gem/lib/app_gem/version.rb
146
146
  - test/fixtures/apps/complex.rb
147
147
  - test/fixtures/apps/concerned/app.rb
148
+ - test/fixtures/apps/concerned/models/catalog_item.rb
149
+ - test/fixtures/apps/concerned/models/filtered_item.rb
148
150
  - test/fixtures/apps/concerned/models/mixins/badmodule.rb
151
+ - test/fixtures/apps/concerned/models/mixins/filterable.rb
152
+ - test/fixtures/apps/concerned/models/mixins/sortable.rb
149
153
  - test/fixtures/apps/custom_dependencies/custom_dependencies.rb
150
154
  - test/fixtures/apps/custom_dependencies/my_dependencies/my_dependency.rb
151
155
  - test/fixtures/apps/demo_app.rb
@@ -226,6 +230,7 @@ files:
226
230
  - test/test_dependencies.rb
227
231
  - test/test_filters.rb
228
232
  - test/test_flash.rb
233
+ - test/test_host_authorization.rb
229
234
  - test/test_launcher.rb
230
235
  - test/test_locale.rb
231
236
  - test/test_logger.rb
@@ -261,111 +266,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
261
266
  - !ruby/object:Gem::Version
262
267
  version: 1.3.6
263
268
  requirements: []
264
- rubygems_version: 3.5.23
269
+ rubygems_version: 3.5.22
265
270
  signing_key:
266
271
  specification_version: 4
267
272
  summary: The required Padrino core gem
268
- test_files:
269
- - test/fixtures/app_gem/Gemfile
270
- - test/fixtures/app_gem/app/app.rb
271
- - test/fixtures/app_gem/app_gem.gemspec
272
- - test/fixtures/app_gem/lib/app_gem.rb
273
- - test/fixtures/app_gem/lib/app_gem/version.rb
274
- - test/fixtures/apps/complex.rb
275
- - test/fixtures/apps/concerned/app.rb
276
- - test/fixtures/apps/concerned/models/mixins/badmodule.rb
277
- - test/fixtures/apps/custom_dependencies/custom_dependencies.rb
278
- - test/fixtures/apps/custom_dependencies/my_dependencies/my_dependency.rb
279
- - test/fixtures/apps/demo_app.rb
280
- - test/fixtures/apps/demo_demo.rb
281
- - test/fixtures/apps/demo_project/api/app.rb
282
- - test/fixtures/apps/demo_project/api/lib/api_lib.rb
283
- - test/fixtures/apps/demo_project/app.rb
284
- - test/fixtures/apps/external_apps/fake_lib.rb
285
- - test/fixtures/apps/external_apps/fake_root.rb
286
- - test/fixtures/apps/external_constants/app.rb
287
- - test/fixtures/apps/external_constants/helpers/external_constants_helpers.rb
288
- - test/fixtures/apps/helpers/class_methods_helpers.rb
289
- - test/fixtures/apps/helpers/instance_methods_helpers.rb
290
- - test/fixtures/apps/helpers/system_helpers.rb
291
- - test/fixtures/apps/kiq.rb
292
- - test/fixtures/apps/lib/myklass.rb
293
- - test/fixtures/apps/lib/myklass/mysubklass.rb
294
- - test/fixtures/apps/models/child.rb
295
- - test/fixtures/apps/models/parent.rb
296
- - test/fixtures/apps/mountable_apps/rack_apps.rb
297
- - test/fixtures/apps/mountable_apps/static.html
298
- - test/fixtures/apps/precompiled_app.rb
299
- - test/fixtures/apps/simple.rb
300
- - test/fixtures/apps/static.rb
301
- - test/fixtures/apps/stealthy/app.rb
302
- - test/fixtures/apps/stealthy/helpers/stealthy_class_helpers.rb
303
- - test/fixtures/apps/system.rb
304
- - test/fixtures/apps/system_class_methods_demo.rb
305
- - test/fixtures/apps/system_instance_methods_demo.rb
306
- - test/fixtures/dependencies/a.rb
307
- - test/fixtures/dependencies/autoload/a.rb
308
- - test/fixtures/dependencies/autoload/b.rb
309
- - test/fixtures/dependencies/autoload/lazy.rb
310
- - test/fixtures/dependencies/b.rb
311
- - test/fixtures/dependencies/c.rb
312
- - test/fixtures/dependencies/circular/e.rb
313
- - test/fixtures/dependencies/circular/f.rb
314
- - test/fixtures/dependencies/circular/g.rb
315
- - test/fixtures/dependencies/d.rb
316
- - test/fixtures/dependencies/gc/t.rb
317
- - test/fixtures/dependencies/gc/u.rb
318
- - test/fixtures/dependencies/gc/v.rb
319
- - test/fixtures/dependencies/gc/w.rb
320
- - test/fixtures/dependencies/gc/x.rb
321
- - test/fixtures/dependencies/gc/y.rb
322
- - test/fixtures/dependencies/linear/h.rb
323
- - test/fixtures/dependencies/linear/i.rb
324
- - test/fixtures/dependencies/nested/j.rb
325
- - test/fixtures/dependencies/nested/k.rb
326
- - test/fixtures/dependencies/nested/l.rb
327
- - test/fixtures/dependencies/nested/m.rb
328
- - test/fixtures/dependencies/nested/n.rb
329
- - test/fixtures/dependencies/nested/ooo.rb
330
- - test/fixtures/dependencies/nested/ppp.rb
331
- - test/fixtures/dependencies/nested/qqq.rb
332
- - test/fixtures/dependencies/nested/rrr.rb
333
- - test/fixtures/dependencies/nested/sss.rb
334
- - test/fixtures/dependencies/parent/admin.rb
335
- - test/fixtures/dependencies/parent/trackable.rb
336
- - test/fixtures/dependencies/parent/user.rb
337
- - test/fixtures/dependencies/partial/a.rb
338
- - test/fixtures/dependencies/partial/b.rb
339
- - test/fixtures/dependencies/reload/forced.rb
340
- - test/fixtures/dependencies/reload/untracked.rb
341
- - test/fixtures/dependencies/reload/untracked_forced.rb
342
- - test/fixtures/dependencies/untracked/a.rb
343
- - test/fixtures/dependencies/untracked/b.rb
344
- - test/fixtures/reloadable_apps/external/app/app.rb
345
- - test/fixtures/reloadable_apps/external/app/controllers/base.rb
346
- - test/fixtures/reloadable_apps/main/app.rb
347
- - test/helper.rb
348
- - test/test_application.rb
349
- - test/test_binstub.rb
350
- - test/test_cli.rb
351
- - test/test_configuration.rb
352
- - test/test_core.rb
353
- - test/test_csrf_protection.rb
354
- - test/test_dependencies.rb
355
- - test/test_filters.rb
356
- - test/test_flash.rb
357
- - test/test_launcher.rb
358
- - test/test_locale.rb
359
- - test/test_logger.rb
360
- - test/test_mounter.rb
361
- - test/test_params_protection.rb
362
- - test/test_rake_tasks.rb
363
- - test/test_reloader_complex.rb
364
- - test/test_reloader_external.rb
365
- - test/test_reloader_simple.rb
366
- - test/test_reloader_storage.rb
367
- - test/test_reloader_system.rb
368
- - test/test_restful_routing.rb
369
- - test/test_router.rb
370
- - test/test_routing.rb
371
- - test/test_server.rb
273
+ test_files: []