rack-test 0.6.3 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/History.md +375 -0
- data/MIT-LICENSE.txt +1 -0
- data/README.md +139 -0
- data/lib/rack/test/cookie_jar.rb +134 -64
- data/lib/rack/test/methods.rb +42 -31
- data/lib/rack/test/uploaded_file.rb +79 -19
- data/lib/rack/test/utils.rb +86 -80
- data/lib/rack/test/version.rb +5 -0
- data/lib/rack/test.rb +268 -204
- metadata +76 -68
- data/.document +0 -4
- data/.gitignore +0 -6
- data/Gemfile +0 -8
- data/Gemfile.lock +0 -41
- data/History.txt +0 -179
- data/README.rdoc +0 -85
- data/Rakefile +0 -33
- data/Thorfile +0 -114
- data/lib/rack/mock_session.rb +0 -66
- data/lib/rack/test/mock_digest_request.rb +0 -29
- data/rack-test.gemspec +0 -77
- data/spec/fixtures/bar.txt +0 -1
- data/spec/fixtures/config.ru +0 -3
- data/spec/fixtures/fake_app.rb +0 -143
- data/spec/fixtures/foo.txt +0 -1
- data/spec/rack/test/cookie_spec.rb +0 -219
- data/spec/rack/test/digest_auth_spec.rb +0 -46
- data/spec/rack/test/multipart_spec.rb +0 -145
- data/spec/rack/test/uploaded_file_spec.rb +0 -24
- data/spec/rack/test/utils_spec.rb +0 -193
- data/spec/rack/test_spec.rb +0 -550
- data/spec/spec_helper.rb +0 -69
- data/spec/support/matchers/body.rb +0 -9
- data/spec/support/matchers/challenge.rb +0 -11
data/lib/rack/test/cookie_jar.rb
CHANGED
@@ -1,132 +1,191 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require 'time'
|
3
5
|
|
4
6
|
module Rack
|
5
7
|
module Test
|
6
|
-
|
8
|
+
# Represents individual cookies in the cookie jar. This is considered private
|
9
|
+
# API and behavior of this class can change at any time.
|
7
10
|
class Cookie # :nodoc:
|
8
11
|
include Rack::Utils
|
9
12
|
|
10
|
-
#
|
11
|
-
attr_reader :name
|
13
|
+
# The name of the cookie, will be a string
|
14
|
+
attr_reader :name
|
15
|
+
|
16
|
+
# The value of the cookie, will be a string or nil if there is no value.
|
17
|
+
attr_reader :value
|
18
|
+
|
19
|
+
# The raw string for the cookie, without options. Will generally be in
|
20
|
+
# name=value format is name and value are provided.
|
21
|
+
attr_reader :raw
|
12
22
|
|
13
|
-
# :api: private
|
14
23
|
def initialize(raw, uri = nil, default_host = DEFAULT_HOST)
|
15
24
|
@default_host = default_host
|
16
25
|
uri ||= default_uri
|
17
26
|
|
18
27
|
# separate the name / value pair from the cookie options
|
19
|
-
@
|
28
|
+
@raw, options = raw.split(/[;,] */n, 2)
|
20
29
|
|
21
|
-
@name, @value = parse_query(@
|
30
|
+
@name, @value = parse_query(@raw, ';').to_a.first
|
22
31
|
@options = parse_query(options, ';')
|
23
32
|
|
24
|
-
@options[
|
25
|
-
|
33
|
+
if domain = @options['domain']
|
34
|
+
@exact_domain_match = false
|
35
|
+
domain[0] = '' if domain[0] == '.'
|
36
|
+
else
|
37
|
+
# If the domain attribute is not present in the cookie,
|
38
|
+
# the domain must match exactly.
|
39
|
+
@exact_domain_match = true
|
40
|
+
@options['domain'] = (uri.host || default_host)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Set the path for the cookie to the directory containing
|
44
|
+
# the request if it isn't set.
|
45
|
+
@options['path'] ||= uri.path.sub(/\/[^\/]*\Z/, '')
|
26
46
|
end
|
27
47
|
|
48
|
+
# Wether the given cookie can replace the current cookie in the cookie jar.
|
28
49
|
def replaces?(other)
|
29
50
|
[name.downcase, domain, path] == [other.name.downcase, other.domain, other.path]
|
30
51
|
end
|
31
52
|
|
32
|
-
#
|
33
|
-
def raw
|
34
|
-
@name_value_raw
|
35
|
-
end
|
36
|
-
|
37
|
-
# :api: private
|
53
|
+
# Whether the cookie has a value.
|
38
54
|
def empty?
|
39
55
|
@value.nil? || @value.empty?
|
40
56
|
end
|
41
57
|
|
42
|
-
#
|
58
|
+
# The explicit or implicit domain for the cookie.
|
43
59
|
def domain
|
44
|
-
@options[
|
60
|
+
@options['domain']
|
45
61
|
end
|
46
62
|
|
63
|
+
# Whether the cookie has the secure flag, indicating it can only be sent over
|
64
|
+
# an encrypted connection.
|
47
65
|
def secure?
|
48
|
-
@options.
|
66
|
+
@options.key?('secure')
|
49
67
|
end
|
50
68
|
|
51
|
-
#
|
69
|
+
# Whether the cookie has the httponly flag, indicating it is not available via
|
70
|
+
# a javascript API.
|
71
|
+
def http_only?
|
72
|
+
@options.key?('HttpOnly') || @options.key?('httponly')
|
73
|
+
end
|
74
|
+
|
75
|
+
# The explicit or implicit path for the cookie.
|
52
76
|
def path
|
53
|
-
|
77
|
+
([*@options['path']].first.split(',').first || '/').strip
|
54
78
|
end
|
55
79
|
|
56
|
-
#
|
80
|
+
# A Time value for when the cookie expires, if the expires option is set.
|
57
81
|
def expires
|
58
|
-
Time.parse(@options[
|
82
|
+
Time.parse(@options['expires']) if @options['expires']
|
59
83
|
end
|
60
84
|
|
61
|
-
#
|
85
|
+
# Whether the cookie is currently expired.
|
62
86
|
def expired?
|
63
87
|
expires && expires < Time.now
|
64
88
|
end
|
65
89
|
|
66
|
-
#
|
90
|
+
# Whether the cookie is valid for the given URI.
|
67
91
|
def valid?(uri)
|
68
92
|
uri ||= default_uri
|
69
93
|
|
70
|
-
if uri.host.nil?
|
71
|
-
uri.host = @default_host
|
72
|
-
end
|
94
|
+
uri.host = @default_host if uri.host.nil?
|
73
95
|
|
74
96
|
real_domain = domain =~ /^\./ ? domain[1..-1] : domain
|
75
|
-
(!secure? || (secure? && uri.scheme ==
|
76
|
-
|
77
|
-
uri.path =~ Regexp.new("^#{Regexp.escape(path)}")
|
97
|
+
!!((!secure? || (secure? && uri.scheme == 'https')) &&
|
98
|
+
uri.host =~ Regexp.new("#{'^' if @exact_domain_match}#{Regexp.escape(real_domain)}$", Regexp::IGNORECASE))
|
78
99
|
end
|
79
100
|
|
80
|
-
#
|
101
|
+
# Cookies that do not match the URI will not be sent in requests to the URI.
|
81
102
|
def matches?(uri)
|
82
|
-
!
|
103
|
+
!expired? && valid?(uri) && uri.path.start_with?(path)
|
83
104
|
end
|
84
105
|
|
85
|
-
#
|
106
|
+
# Order cookies by name, path, and domain.
|
86
107
|
def <=>(other)
|
87
|
-
# Orders the cookies from least specific to most
|
88
108
|
[name, path, domain.reverse] <=> [other.name, other.path, other.domain.reverse]
|
89
109
|
end
|
90
110
|
|
91
|
-
|
111
|
+
# A hash of cookie options, including the cookie value, but excluding the cookie name.
|
112
|
+
def to_h
|
113
|
+
@options.merge(
|
114
|
+
'value' => @value,
|
115
|
+
'HttpOnly' => http_only?,
|
116
|
+
'secure' => secure?
|
117
|
+
)
|
118
|
+
end
|
119
|
+
alias to_hash to_h
|
120
|
+
|
121
|
+
private
|
92
122
|
|
123
|
+
# The default URI to use for the cookie, including just the host.
|
93
124
|
def default_uri
|
94
|
-
URI.parse(
|
125
|
+
URI.parse('//' + @default_host + '/')
|
95
126
|
end
|
96
|
-
|
97
127
|
end
|
98
128
|
|
129
|
+
# Represents all cookies for a session, handling adding and
|
130
|
+
# removing cookies, and finding which cookies apply to a given
|
131
|
+
# request. This is considered private API and behavior of this
|
132
|
+
# class can change at any time.
|
99
133
|
class CookieJar # :nodoc:
|
134
|
+
DELIMITER = '; '.freeze
|
100
135
|
|
101
|
-
# :api: private
|
102
136
|
def initialize(cookies = [], default_host = DEFAULT_HOST)
|
103
137
|
@default_host = default_host
|
104
|
-
@cookies = cookies
|
105
|
-
|
138
|
+
@cookies = cookies.sort!
|
139
|
+
end
|
140
|
+
|
141
|
+
# Ensure the copy uses a distinct cookies array.
|
142
|
+
def initialize_copy(other)
|
143
|
+
super
|
144
|
+
@cookies = @cookies.dup
|
106
145
|
end
|
107
146
|
|
147
|
+
# Return the value for first cookie with the given name, or nil
|
148
|
+
# if no such cookie exists.
|
108
149
|
def [](name)
|
109
|
-
|
110
|
-
|
111
|
-
|
150
|
+
name = name.to_s
|
151
|
+
@cookies.each do |cookie|
|
152
|
+
return cookie.value if cookie.name == name
|
153
|
+
end
|
154
|
+
nil
|
112
155
|
end
|
113
156
|
|
157
|
+
# Set a cookie with the given name and value in the
|
158
|
+
# cookie jar.
|
114
159
|
def []=(name, value)
|
115
160
|
merge("#{name}=#{Rack::Utils.escape(value)}")
|
116
161
|
end
|
117
162
|
|
163
|
+
# Return the first cookie with the given name, or nil if
|
164
|
+
# no such cookie exists.
|
165
|
+
def get_cookie(name)
|
166
|
+
@cookies.each do |cookie|
|
167
|
+
return cookie if cookie.name == name
|
168
|
+
end
|
169
|
+
nil
|
170
|
+
end
|
171
|
+
|
172
|
+
# Delete all cookies with the given name from the cookie jar.
|
118
173
|
def delete(name)
|
119
174
|
@cookies.reject! do |cookie|
|
120
175
|
cookie.name == name
|
121
176
|
end
|
177
|
+
nil
|
122
178
|
end
|
123
179
|
|
180
|
+
# Add a string of raw cookie information to the cookie jar,
|
181
|
+
# if the cookie is valid for the given URI.
|
182
|
+
# Cookies should be separated with a newline.
|
124
183
|
def merge(raw_cookies, uri = nil)
|
125
184
|
return unless raw_cookies
|
126
185
|
|
127
186
|
if raw_cookies.is_a? String
|
128
187
|
raw_cookies = raw_cookies.split("\n")
|
129
|
-
raw_cookies.reject!
|
188
|
+
raw_cookies.reject!(&:empty?)
|
130
189
|
end
|
131
190
|
|
132
191
|
raw_cookies.each do |raw_cookie|
|
@@ -135,6 +194,7 @@ module Rack
|
|
135
194
|
end
|
136
195
|
end
|
137
196
|
|
197
|
+
# Add a Cookie to the cookie jar.
|
138
198
|
def <<(new_cookie)
|
139
199
|
@cookies.reject! do |existing_cookie|
|
140
200
|
new_cookie.replaces?(existing_cookie)
|
@@ -144,39 +204,49 @@ module Rack
|
|
144
204
|
@cookies.sort!
|
145
205
|
end
|
146
206
|
|
147
|
-
#
|
207
|
+
# Return a raw cookie string for the cookie header to
|
208
|
+
# use for the given URI.
|
148
209
|
def for(uri)
|
149
|
-
|
210
|
+
buf = String.new
|
211
|
+
delimiter = nil
|
212
|
+
|
213
|
+
each_cookie_for(uri) do |cookie|
|
214
|
+
if delimiter
|
215
|
+
buf << delimiter
|
216
|
+
else
|
217
|
+
delimiter = DELIMITER
|
218
|
+
end
|
219
|
+
buf << cookie.raw
|
220
|
+
end
|
221
|
+
|
222
|
+
buf
|
150
223
|
end
|
151
224
|
|
225
|
+
# Return a hash cookie names and cookie values for cookies in the jar.
|
152
226
|
def to_hash
|
153
227
|
cookies = {}
|
154
228
|
|
155
|
-
|
156
|
-
cookies[name] = cookie.value
|
229
|
+
@cookies.each do |cookie|
|
230
|
+
cookies[cookie.name] = cookie.value
|
157
231
|
end
|
158
232
|
|
159
|
-
|
233
|
+
cookies
|
160
234
|
end
|
161
235
|
|
162
|
-
|
163
|
-
|
164
|
-
def hash_for(uri = nil)
|
165
|
-
cookies = {}
|
236
|
+
private
|
166
237
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
238
|
+
# Yield each cookie that matches for the URI.
|
239
|
+
#
|
240
|
+
# The cookies are sorted by most specific first. So, we loop through
|
241
|
+
# all the cookies in order and add it to a hash by cookie name if
|
242
|
+
# the cookie can be sent to the current URI. It's added to the hash
|
243
|
+
# so that when we are done, the cookies will be unique by name and
|
244
|
+
# we'll have grabbed the most specific to the URI.
|
245
|
+
def each_cookie_for(uri)
|
172
246
|
@cookies.each do |cookie|
|
173
|
-
|
247
|
+
yield cookie if !uri || cookie.matches?(uri)
|
174
248
|
end
|
175
|
-
|
176
|
-
return cookies
|
177
249
|
end
|
178
|
-
|
179
250
|
end
|
180
|
-
|
181
251
|
end
|
182
252
|
end
|
data/lib/rack/test/methods.rb
CHANGED
@@ -1,12 +1,16 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
2
4
|
|
3
5
|
module Rack
|
4
6
|
module Test
|
5
|
-
|
6
7
|
# This module serves as the primary integration point for using Rack::Test
|
7
8
|
# in a testing environment. It depends on an app method being defined in the
|
8
9
|
# same context, and provides the Rack::Test API methods (see Rack::Test::Session
|
9
|
-
# for their documentation).
|
10
|
+
# for their documentation). It defines the following methods that are delegated
|
11
|
+
# to the current session: :request, :get, :post, :put, :patch, :delete, :options,
|
12
|
+
# :head, :custom_request, :follow_redirect!, :header, :env, :set_cookie,
|
13
|
+
# :clear_cookies, :authorize, :basic_authorize, :last_response, and :last_request.
|
10
14
|
#
|
11
15
|
# Example:
|
12
16
|
#
|
@@ -14,23 +18,14 @@ module Rack
|
|
14
18
|
# include Rack::Test::Methods
|
15
19
|
#
|
16
20
|
# def app
|
17
|
-
# MyApp
|
21
|
+
# MyApp
|
18
22
|
# end
|
19
23
|
# end
|
20
24
|
module Methods
|
21
25
|
extend Forwardable
|
22
26
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
@_rack_mock_sessions ||= {}
|
27
|
-
@_rack_mock_sessions[name] ||= build_rack_mock_session
|
28
|
-
end
|
29
|
-
|
30
|
-
def build_rack_mock_session # :nodoc:
|
31
|
-
Rack::MockSession.new(app)
|
32
|
-
end
|
33
|
-
|
27
|
+
# Return the existing session with the given name, or a new
|
28
|
+
# rack session. Always use a new session if name is nil.
|
34
29
|
def rack_test_session(name = :default) # :nodoc:
|
35
30
|
return build_rack_test_session(name) unless name
|
36
31
|
|
@@ -38,25 +33,39 @@ module Rack
|
|
38
33
|
@_rack_test_sessions[name] ||= build_rack_test_session(name)
|
39
34
|
end
|
40
35
|
|
41
|
-
|
42
|
-
|
43
|
-
end
|
36
|
+
# For backwards compatibility with older rack-test versions.
|
37
|
+
alias rack_mock_session rack_test_session # :nodoc:
|
44
38
|
|
45
|
-
|
46
|
-
|
39
|
+
# Create a new Rack::Test::Session for #app.
|
40
|
+
def build_rack_test_session(_name) # :nodoc:
|
41
|
+
if respond_to?(:build_rack_mock_session, true)
|
42
|
+
# Backwards compatibility for capybara
|
43
|
+
build_rack_mock_session
|
44
|
+
else
|
45
|
+
if respond_to?(:default_host)
|
46
|
+
Session.new(app, default_host)
|
47
|
+
else
|
48
|
+
Session.new(app)
|
49
|
+
end
|
50
|
+
end
|
47
51
|
end
|
48
52
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
+
# Return the currently actively session. This is the session to
|
54
|
+
# which the delegated methods are sent.
|
55
|
+
def current_session
|
56
|
+
@_rack_test_current_session ||= rack_test_session
|
53
57
|
end
|
54
58
|
|
55
|
-
|
56
|
-
|
59
|
+
# Create a new session (or reuse an existing session with the given name),
|
60
|
+
# and make it the current session for the given block.
|
61
|
+
def with_session(name)
|
62
|
+
session = _rack_test_current_session
|
63
|
+
yield(@_rack_test_current_session = rack_test_session(name))
|
64
|
+
ensure
|
65
|
+
@_rack_test_current_session = session
|
57
66
|
end
|
58
67
|
|
59
|
-
|
68
|
+
def_delegators(:current_session,
|
60
69
|
:request,
|
61
70
|
:get,
|
62
71
|
:post,
|
@@ -65,6 +74,7 @@ module Rack
|
|
65
74
|
:delete,
|
66
75
|
:options,
|
67
76
|
:head,
|
77
|
+
:custom_request,
|
68
78
|
:follow_redirect!,
|
69
79
|
:header,
|
70
80
|
:env,
|
@@ -72,12 +82,13 @@ module Rack
|
|
72
82
|
:clear_cookies,
|
73
83
|
:authorize,
|
74
84
|
:basic_authorize,
|
75
|
-
:digest_authorize,
|
76
85
|
:last_response,
|
77
|
-
:last_request
|
78
|
-
|
86
|
+
:last_request,
|
87
|
+
)
|
79
88
|
|
80
|
-
|
89
|
+
# Private accessor to avoid uninitialized instance variable warning in Ruby 2.*
|
90
|
+
attr_accessor :_rack_test_current_session
|
91
|
+
private :_rack_test_current_session
|
81
92
|
end
|
82
93
|
end
|
83
94
|
end
|
@@ -1,16 +1,17 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'tempfile'
|
5
|
+
require 'stringio'
|
3
6
|
|
4
7
|
module Rack
|
5
8
|
module Test
|
6
|
-
|
7
9
|
# Wraps a Tempfile with a content type. Including one or more UploadedFile's
|
8
10
|
# in the params causes Rack::Test to build and issue a multipart request.
|
9
11
|
#
|
10
12
|
# Example:
|
11
13
|
# post "/photos", "file" => Rack::Test::UploadedFile.new("me.jpg", "image/jpeg")
|
12
14
|
class UploadedFile
|
13
|
-
|
14
15
|
# The filename, *not* including the path, of the "uploaded" file
|
15
16
|
attr_reader :original_filename
|
16
17
|
|
@@ -20,34 +21,93 @@ module Rack
|
|
20
21
|
# The content type of the "uploaded" file
|
21
22
|
attr_accessor :content_type
|
22
23
|
|
23
|
-
|
24
|
-
|
25
|
-
|
24
|
+
# Creates a new UploadedFile instance.
|
25
|
+
#
|
26
|
+
# Arguments:
|
27
|
+
# content :: is a path to a file, or an {IO} or {StringIO} object representing the content.
|
28
|
+
# content_type :: MIME type of the file
|
29
|
+
# binary :: Whether the file should be set to binmode (content treated as binary).
|
30
|
+
# original_filename :: The filename to use for the file. Required if content is StringIO, optional override if not
|
31
|
+
def initialize(content, content_type = 'text/plain', binary = false, original_filename: nil)
|
26
32
|
@content_type = content_type
|
27
|
-
@original_filename =
|
33
|
+
@original_filename = original_filename
|
28
34
|
|
29
|
-
|
30
|
-
|
31
|
-
|
35
|
+
case content
|
36
|
+
when StringIO
|
37
|
+
initialize_from_stringio(content)
|
38
|
+
else
|
39
|
+
initialize_from_file_path(content)
|
40
|
+
end
|
32
41
|
|
33
|
-
|
42
|
+
@tempfile.binmode if binary
|
34
43
|
end
|
35
44
|
|
45
|
+
# The path to the tempfile. Will not work if the receiver's content is from a StringIO.
|
36
46
|
def path
|
37
|
-
|
47
|
+
tempfile.path
|
48
|
+
end
|
49
|
+
alias local_path path
|
50
|
+
|
51
|
+
# Delegate all methods not handled to the tempfile.
|
52
|
+
def method_missing(method_name, *args, &block)
|
53
|
+
tempfile.public_send(method_name, *args, &block)
|
38
54
|
end
|
39
55
|
|
40
|
-
|
56
|
+
# Append to given buffer in 64K chunks to avoid multiple large
|
57
|
+
# copies of file data in memory. Rewind tempfile before and
|
58
|
+
# after to make sure all data in tempfile is appended to the
|
59
|
+
# buffer.
|
60
|
+
def append_to(buffer)
|
61
|
+
tempfile.rewind
|
62
|
+
|
63
|
+
buf = String.new
|
64
|
+
buffer << tempfile.readpartial(65_536, buf) until tempfile.eof?
|
41
65
|
|
42
|
-
|
43
|
-
|
66
|
+
tempfile.rewind
|
67
|
+
|
68
|
+
nil
|
44
69
|
end
|
45
70
|
|
46
|
-
def
|
47
|
-
|
71
|
+
def respond_to_missing?(method_name, include_private = false) #:nodoc:
|
72
|
+
tempfile.respond_to?(method_name, include_private) || super
|
48
73
|
end
|
49
74
|
|
50
|
-
|
75
|
+
# A proc that can be used as a finalizer to close and unlink the tempfile.
|
76
|
+
def self.finalize(file)
|
77
|
+
proc { actually_finalize file }
|
78
|
+
end
|
51
79
|
|
80
|
+
# Close and unlink the given file, used as a finalizer for the tempfile,
|
81
|
+
# if the tempfile is backed by a file in the filesystem.
|
82
|
+
def self.actually_finalize(file)
|
83
|
+
file.close
|
84
|
+
file.unlink
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
# Use the StringIO as the tempfile.
|
90
|
+
def initialize_from_stringio(stringio)
|
91
|
+
raise(ArgumentError, 'Missing `original_filename` for StringIO object') unless @original_filename
|
92
|
+
|
93
|
+
@tempfile = stringio
|
94
|
+
end
|
95
|
+
|
96
|
+
# Create a tempfile and copy the content from the given path into the tempfile, optionally renaming if
|
97
|
+
# original_filename has been set.
|
98
|
+
def initialize_from_file_path(path)
|
99
|
+
raise "#{path} file does not exist" unless ::File.exist?(path)
|
100
|
+
|
101
|
+
@original_filename ||= ::File.basename(path)
|
102
|
+
extension = ::File.extname(@original_filename)
|
103
|
+
|
104
|
+
@tempfile = Tempfile.new([::File.basename(@original_filename, extension), extension])
|
105
|
+
@tempfile.set_encoding(Encoding::BINARY)
|
106
|
+
|
107
|
+
ObjectSpace.define_finalizer(self, self.class.finalize(@tempfile))
|
108
|
+
|
109
|
+
FileUtils.copy_file(path, @tempfile.path)
|
110
|
+
end
|
111
|
+
end
|
52
112
|
end
|
53
113
|
end
|