hurley-http-cache 0.1.0.beta
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 +7 -0
- data/LICENSE +13 -0
- data/README.md +93 -0
- data/lib/hurley-http-cache.rb +1 -0
- data/lib/hurley/http_cache.rb +242 -0
- data/lib/hurley/http_cache/cache_control.rb +118 -0
- data/lib/hurley/http_cache/request.rb +51 -0
- data/lib/hurley/http_cache/response.rb +186 -0
- data/lib/hurley/http_cache/storage.rb +182 -0
- data/test/http_cache/cache_control_test.rb +79 -0
- data/test/http_cache/request_test.rb +74 -0
- data/test/http_cache/response_test.rb +116 -0
- data/test/http_cache/storage_test.rb +80 -0
- data/test/live_http_cache_test.rb +238 -0
- data/test/support/server.rb +103 -0
- data/test/test_helper.rb +23 -0
- metadata +114 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
require File.expand_path('../../test_helper', __FILE__)
|
2
|
+
|
3
|
+
class RequestTest < MiniTest::Test
|
4
|
+
def test_GET_requests_are_cacheable
|
5
|
+
request = new_request
|
6
|
+
|
7
|
+
assert_predicate request, :cacheable?
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_HEAD_requests_are_cacheable
|
11
|
+
request = new_request(verb: :head)
|
12
|
+
|
13
|
+
assert_predicate request, :cacheable?
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_POST_requests_are_not_cacheable
|
17
|
+
request = new_request(verb: :post)
|
18
|
+
|
19
|
+
refute_predicate request, :cacheable?
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_PUT_requests_are_not_cacheable
|
23
|
+
request = new_request(verb: :put)
|
24
|
+
|
25
|
+
refute_predicate request, :cacheable?
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_OPTIONS_requests_are_not_cacheable
|
29
|
+
request = new_request(verb: :options)
|
30
|
+
|
31
|
+
refute_predicate request, :cacheable?
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_DELETE_requests_are_not_cacheable
|
35
|
+
request = new_request(verb: :delete)
|
36
|
+
|
37
|
+
refute_predicate request, :cacheable?
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_TRACE_requests_are_not_cacheable
|
41
|
+
request = new_request(verb: :trace)
|
42
|
+
|
43
|
+
refute_predicate request, :cacheable?
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_no_store_requests_are_not_cacheable
|
47
|
+
request = new_request(header: { 'Cache-Control' => 'no-store' })
|
48
|
+
|
49
|
+
refute_predicate request, :cacheable?
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_no_cache
|
53
|
+
request = new_request(header: { 'Cache-Control' => 'no-cache' })
|
54
|
+
|
55
|
+
assert_predicate request, :no_cache?
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_serializable_hash
|
59
|
+
request = new_request(header: { 'User-Agent' => 'FooBar' })
|
60
|
+
hash = request.serializable_hash
|
61
|
+
|
62
|
+
assert_equal 'get', hash['verb']
|
63
|
+
assert_equal 'http://test.com', hash['url']
|
64
|
+
assert_instance_of Hash, hash['header']
|
65
|
+
assert_equal({ 'User-Agent' => 'FooBar' }, hash['header'])
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def new_request(verb: :get, url: 'http://test.com', header: {})
|
71
|
+
req = Hurley::Request.new(verb, url, Hurley::Header.new(header))
|
72
|
+
Hurley::HttpCache::Request.new(req)
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require File.expand_path('../../test_helper', __FILE__)
|
2
|
+
|
3
|
+
class ResponseTest < MiniTest::Test
|
4
|
+
def test_not_cacheable_in_shared_cache_if_the_response_is_private
|
5
|
+
response = new_response(header: { 'Cache-Control' => 'private, max-age=400' })
|
6
|
+
|
7
|
+
refute_predicate response, :cacheable_in_shared_cache?
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_not_cacheable_in_shared_cache_if_the_response_is_no_store
|
11
|
+
response = new_response(header: { 'Cache-Control' => 'no-store, max-age=400' })
|
12
|
+
|
13
|
+
refute_predicate response, :cacheable_in_shared_cache?
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_not_cacheable_in_shared_cache_if_the_status_code_is_not_acceptable
|
17
|
+
response = new_response(status_code: 503, header: { 'Cache-Control' => 'max-age=400' })
|
18
|
+
|
19
|
+
refute_predicate response, :cacheable_in_shared_cache?
|
20
|
+
end
|
21
|
+
|
22
|
+
[200, 203, 300, 301, 302, 404, 410].each do |status|
|
23
|
+
define_method("test_response_is_cacheable_in_shared_cache_if_status_code_is_#{status}") do
|
24
|
+
response = new_response(status_code: status, header: { 'Cache-Control' => 'max-age=400' })
|
25
|
+
|
26
|
+
assert_predicate response, :cacheable_in_shared_cache?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_the_response_is_cacheable_if_the_response_is_marked_as_private
|
31
|
+
response = new_response(header: { 'Cache-Control' => 'private, max-age=400' })
|
32
|
+
|
33
|
+
assert_predicate response, :cacheable_in_private_cache?
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_the_response_is_not_cacheable_if_it_should_not_be_stored
|
37
|
+
response = new_response(header: { 'Cache-Control' => 'no-store, max-age=400' })
|
38
|
+
|
39
|
+
refute_predicate response, :cacheable_in_private_cache?
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_the_response_is_not_cacheable_when_the_status_code_is_not_acceptable
|
43
|
+
response = new_response(status_code: 503, header: { 'Cache-Control' => 'max-age=400' })
|
44
|
+
|
45
|
+
refute_predicate response, :cacheable_in_private_cache?
|
46
|
+
end
|
47
|
+
|
48
|
+
[200, 203, 300, 301, 302, 404, 410].each do |status|
|
49
|
+
define_method("test_response_is_cacheable_in_private_cache_if_status_code_is_#{status}") do
|
50
|
+
headers = { 'Cache-Control' => 'max-age=400' }
|
51
|
+
response = new_response(status_code: status, header: headers)
|
52
|
+
|
53
|
+
assert_predicate response, :cacheable_in_private_cache?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_response_is_fresh
|
58
|
+
date = (Time.now - 200).httpdate
|
59
|
+
response = new_response(header: { 'Cache-Control' => 'max-age=400', 'Date' => date })
|
60
|
+
|
61
|
+
assert_predicate response, :fresh?
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_ttl_expired
|
65
|
+
date = (Time.now - 500).httpdate
|
66
|
+
response = new_response(header: { 'Cache-Control' => 'max-age=400', 'Date' => date })
|
67
|
+
|
68
|
+
refute_predicate response, :fresh?
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_response_not_modified
|
72
|
+
response = new_response(status_code: 304)
|
73
|
+
|
74
|
+
assert_predicate response, :not_modified?
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_response_last_modified
|
78
|
+
response = new_response(header: { 'Last-Modified' => '123' })
|
79
|
+
|
80
|
+
assert_equal '123', response.last_modified
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_etag
|
84
|
+
response = new_response(header: { 'ETag' => 'tag' })
|
85
|
+
|
86
|
+
assert_equal 'tag', response.etag
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_restore
|
90
|
+
payload = { 'status_code' => 200, 'header' => { 'X-Foo' => '123' }, 'body' => 'ohai' }
|
91
|
+
response = Hurley::HttpCache::Response.restore(Hurley::Request.new, payload)
|
92
|
+
|
93
|
+
assert_kind_of Hurley::HttpCache::Response, response
|
94
|
+
assert_equal 200, response.status_code
|
95
|
+
assert_instance_of Hurley::Header, response.header
|
96
|
+
assert_equal 'ohai', response.body
|
97
|
+
end
|
98
|
+
|
99
|
+
def test_serializable_hash
|
100
|
+
response = new_response(body: 'ohai')
|
101
|
+
hash = response.serializable_hash
|
102
|
+
|
103
|
+
assert_kind_of Hash, hash['header']
|
104
|
+
assert_equal 200, hash['status_code']
|
105
|
+
assert_equal 'ohai', hash['body']
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def new_response(status_code: 200, header: {}, body: '')
|
111
|
+
request = Hurley::Request.new
|
112
|
+
response = Hurley::Response.new(request, status_code, Hurley::Header.new(header))
|
113
|
+
response.body = body
|
114
|
+
Hurley::HttpCache::Response.new(response)
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require File.expand_path('../../test_helper', __FILE__)
|
2
|
+
|
3
|
+
class StorageSetupTest < MiniTest::Test
|
4
|
+
def test_raises_an_error_when_the_given_store_is_not_valid
|
5
|
+
assert_raises(ArgumentError) {
|
6
|
+
Hurley::HttpCache::Storage.new(store: Object.new)
|
7
|
+
}
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module StorageTests
|
12
|
+
def setup
|
13
|
+
@logger = TestLogger.new
|
14
|
+
@request = Hurley::HttpCache::Request.new(Hurley::Request.new(:get, 'http://test.com', Hurley::Header.new))
|
15
|
+
@response = Hurley::HttpCache::Response.new(Hurley::Response.new(@request))
|
16
|
+
@storage = Hurley::HttpCache::Storage.new(serializer: serializer, logger: @logger)
|
17
|
+
end
|
18
|
+
|
19
|
+
def serializer
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
def cache_key
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_writes_the_response_object_to_the_underlying_cache
|
28
|
+
@storage.write(@request, @response)
|
29
|
+
assert_kind_of Array, @storage.cache.read(cache_key)
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_retrieves_the_response_from_the_cache
|
33
|
+
@storage.write(@request, @response)
|
34
|
+
refute_nil @storage.read(@request)
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_returns_nil_if_the_response_is_not_cached
|
38
|
+
assert_nil @storage.read(@request)
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_removes_the_entries_from_the_cache_of_the_given_URL
|
42
|
+
@storage.write(@request, @response)
|
43
|
+
@storage.delete(@request.url)
|
44
|
+
assert_nil @storage.read(@request)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class JSONStorageTest < MiniTest::Test
|
49
|
+
include StorageTests
|
50
|
+
|
51
|
+
def test_raises_when_ascii_cant_be_converted
|
52
|
+
@response.body = "\u2665".force_encoding('ASCII-8BIT')
|
53
|
+
|
54
|
+
assert_raises(Encoding::UndefinedConversionError) {
|
55
|
+
@storage.write(@request, @response)
|
56
|
+
}
|
57
|
+
|
58
|
+
assert_includes @logger.warns, "Response could not be serialized: \"\\xE2\" from ASCII-8BIT to UTF-8. Try using Marshal to serialize."
|
59
|
+
end
|
60
|
+
|
61
|
+
def cache_key
|
62
|
+
'e572976e5e9c037a9a436d280faa5aa102ca1683'
|
63
|
+
end
|
64
|
+
|
65
|
+
def serializer
|
66
|
+
JSON
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class MarshalStorageTest < MiniTest::Test
|
71
|
+
include StorageTests
|
72
|
+
|
73
|
+
def cache_key
|
74
|
+
'cbc594f12e773090622daa7976585aa8e15ff7dc'
|
75
|
+
end
|
76
|
+
|
77
|
+
def serializer
|
78
|
+
Marshal
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
require 'hurley/test/integration'
|
3
|
+
|
4
|
+
class HttpCacheTest < MiniTest::Test
|
5
|
+
Hurley::Test::Integration.apply(self)
|
6
|
+
|
7
|
+
def setup
|
8
|
+
client.delete('/')
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_caches_GET_requests
|
12
|
+
client.get('counter')
|
13
|
+
|
14
|
+
assert_equal '1', client.get('counter').body
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_logs_that_a_GET_response_is_stored
|
18
|
+
client.get('counter')
|
19
|
+
|
20
|
+
assert_includes logger.debugs, 'HTTP Cache: [GET /counter] miss, store'
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_does_not_cache_POST_requests
|
24
|
+
client.post('counter')
|
25
|
+
|
26
|
+
assert_equal '2', client.post('counter').body
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_does_not_cache_responses_with_invalid_status_code
|
30
|
+
client.get('broken')
|
31
|
+
|
32
|
+
assert_equal '2', client.get('broken').body
|
33
|
+
end
|
34
|
+
|
35
|
+
%w(post put patch delete).each do |verb|
|
36
|
+
define_method("test_expires_#{verb.upcase}_requests") do
|
37
|
+
client.get('counter')
|
38
|
+
client.send(verb, 'counter')
|
39
|
+
|
40
|
+
assert_equal '3', client.get('counter').body
|
41
|
+
end
|
42
|
+
|
43
|
+
define_method("test_logs_that_a_#{verb.upcase}_request_was_deleted_from_cache") do
|
44
|
+
client.send(verb, 'counter')
|
45
|
+
|
46
|
+
assert_includes logger.debugs, "HTTP Cache: [#{verb.upcase} /counter] unacceptable, delete"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_expires_entries_for_the_Location_header
|
51
|
+
client.get('counter')
|
52
|
+
client.post('delete-with-location')
|
53
|
+
|
54
|
+
assert_equal '2', client.get('counter').body
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_expires_entries_for_the_Content_Location_header
|
58
|
+
client.get('counter')
|
59
|
+
client.post('delete-with-content-location')
|
60
|
+
|
61
|
+
assert_equal '2', client.get('counter').body
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_does_not_cache_responses_with_a_explicit_not_store_directive
|
65
|
+
client.get('dontstore')
|
66
|
+
|
67
|
+
assert_equal '2', client.get('dontstore').body
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_logs_that_a_response_with_a_no_store_directive_is_invalid
|
71
|
+
client.get('dontstore')
|
72
|
+
|
73
|
+
assert_includes logger.debugs, 'HTTP Cache: [GET /dontstore] miss, invalid'
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_does_not_caches_multiple_responses_when_the_headers_differ
|
77
|
+
client.get('counter') { |req| req.header[:accept] = 'text/html' }
|
78
|
+
|
79
|
+
assert_equal '1', (client.get('counter') { |req| req.header[:accept] = 'text/html' }).body
|
80
|
+
assert_equal '1', (client.get('counter') { |req| req.header[:accept] = 'application/json' }).body
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_caches_multiples_responses_based_on_the_Vary_header
|
84
|
+
client.get('vary') { |req| req.header['User-Agent'] = 'Agent/1.0' }
|
85
|
+
|
86
|
+
assert_equal '1', (client.get('vary') { |req| req.header['User-Agent'] = 'Agent/1.0' }).body
|
87
|
+
assert_equal '2', (client.get('vary') { |req| req.header['User-Agent'] = 'Agent/2.0' }).body
|
88
|
+
assert_equal '3', (client.get('vary') { |req| req.header['User-Agent'] = 'Agent/3.0' }).body
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_never_caches_responses_with_the_wildcard_Vary_header
|
92
|
+
client.get('vary-wildcard')
|
93
|
+
|
94
|
+
assert_equal '2', client.get('vary-wildcard').body
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_caches_requests_with_the_Expires_header
|
98
|
+
client.get('expires')
|
99
|
+
|
100
|
+
assert_equal '1', client.get('expires').body
|
101
|
+
end
|
102
|
+
|
103
|
+
def test_logs_that_a_request_with_the_Expires_is_fresh_and_stored
|
104
|
+
client.get('expires')
|
105
|
+
|
106
|
+
assert_includes logger.debugs, 'HTTP Cache: [GET /expires] miss, store'
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_differs_requests_with_different_query_strings_in_the_log
|
110
|
+
client.get('counter')
|
111
|
+
client.get('counter', q: 'what')
|
112
|
+
|
113
|
+
assert_includes logger.debugs, 'HTTP Cache: [GET /counter] miss, store'
|
114
|
+
assert_includes logger.debugs, 'HTTP Cache: [GET /counter?q=what] miss, store'
|
115
|
+
end
|
116
|
+
|
117
|
+
def test_logs_that_a_stored_GET_response_is_fresh
|
118
|
+
client.get('counter')
|
119
|
+
client.get('counter')
|
120
|
+
|
121
|
+
assert_includes logger.debugs, 'HTTP Cache: [GET /counter] fresh'
|
122
|
+
end
|
123
|
+
|
124
|
+
def test_maintains_the_Date_header_for_cached_responses
|
125
|
+
first_date = client.get('counter').header['Date']
|
126
|
+
second_date = client.get('counter').header['Date']
|
127
|
+
|
128
|
+
assert_equal second_date, first_date
|
129
|
+
end
|
130
|
+
|
131
|
+
def test_preserves_an_old_Date_header_if_present
|
132
|
+
date = client.get('old').header['Date']
|
133
|
+
|
134
|
+
assert_match(/^\w{3}, \d{2} \w{3} \d{4} \d{2}:\d{2}:\d{2} GMT\z/, date)
|
135
|
+
end
|
136
|
+
|
137
|
+
def test_sends_the_Last_Modified_header_on_response_validation
|
138
|
+
client.get('timestamped')
|
139
|
+
|
140
|
+
assert_equal '1', client.get('timestamped').body
|
141
|
+
end
|
142
|
+
#
|
143
|
+
def test_logs_that_the_request_with_Last_Modified_was_revalidated
|
144
|
+
client.get('timestamped')
|
145
|
+
|
146
|
+
assert_equal '1', client.get('timestamped').body
|
147
|
+
assert_includes logger.debugs, 'HTTP Cache: [GET /timestamped] valid, store'
|
148
|
+
end
|
149
|
+
|
150
|
+
def test_sends_the_If_None_Match_header_on_response_validation
|
151
|
+
client.get('etag')
|
152
|
+
|
153
|
+
assert_equal '1', client.get('etag').body
|
154
|
+
end
|
155
|
+
|
156
|
+
def test_logs_that_the_request_with_ETag_was_revalidated
|
157
|
+
client.get('etag')
|
158
|
+
|
159
|
+
assert_equal '1', client.get('etag').body
|
160
|
+
assert_includes logger.debugs, 'HTTP Cache: [GET /etag] valid, store'
|
161
|
+
end
|
162
|
+
|
163
|
+
def test_updates_the_Cache_Control_header_when_a_response_is_validated
|
164
|
+
first_cache_control = client.get('etag').header['Cache-Control']
|
165
|
+
second_cache_control = client.get('etag').header['Cache-Control']
|
166
|
+
|
167
|
+
refute_equal first_cache_control, second_cache_control
|
168
|
+
end
|
169
|
+
|
170
|
+
def test_updates_the_Date_header_when_a_response_is_validated
|
171
|
+
first_date = client.get('etag').header['Date']
|
172
|
+
second_date = client.get('etag').header['Date']
|
173
|
+
|
174
|
+
refute_equal first_date, second_date
|
175
|
+
end
|
176
|
+
|
177
|
+
def test_updates_the_Expires_header_when_a_response_is_validated
|
178
|
+
first_expires = client.get('etag').header['Expires']
|
179
|
+
second_expires = client.get('etag').header['Expires']
|
180
|
+
|
181
|
+
refute_equal first_expires, second_expires
|
182
|
+
end
|
183
|
+
#
|
184
|
+
def test_updates_the_Vary_header_when_a_response_is_validated
|
185
|
+
first_vary = client.get('etag').header['Vary']
|
186
|
+
second_vary = client.get('etag').header['Vary']
|
187
|
+
|
188
|
+
refute_equal first_vary, second_vary
|
189
|
+
end
|
190
|
+
|
191
|
+
def test_shared_cache_does_not_cache_private_requests
|
192
|
+
client.get('private')
|
193
|
+
|
194
|
+
assert_equal '2', client.get('private').body
|
195
|
+
end
|
196
|
+
|
197
|
+
def test_shared_cache_logs_invalid_request
|
198
|
+
client.get('private')
|
199
|
+
|
200
|
+
assert_includes logger.debugs, 'HTTP Cache: [GET /private] miss, invalid'
|
201
|
+
end
|
202
|
+
|
203
|
+
def test_private_cache_caches_private_requests
|
204
|
+
client.connection = Hurley::HttpCache.new(logger: logger, shared_cache: false)
|
205
|
+
client.get('private')
|
206
|
+
|
207
|
+
assert_equal '1', client.get('private').body
|
208
|
+
end
|
209
|
+
|
210
|
+
def test_private_cache_logs_valid_requests
|
211
|
+
client.connection = Hurley::HttpCache.new(logger: logger, shared_cache: false)
|
212
|
+
client.get('private')
|
213
|
+
|
214
|
+
assert_includes logger.debugs, 'HTTP Cache: [GET /private] miss, store'
|
215
|
+
end
|
216
|
+
|
217
|
+
def test_no_cache_bypasses_cache
|
218
|
+
client.get('counter')
|
219
|
+
|
220
|
+
assert_equal '2', (client.get('counter') { |req| req.header['Cache-Control'] = 'no-cache' }).body
|
221
|
+
end
|
222
|
+
|
223
|
+
def test_no_cache_caches_the_response
|
224
|
+
client.get('counter') { |req| req.header['Cache-Control'] = 'no-cache' }
|
225
|
+
|
226
|
+
assert_equal '1', client.get('counter').body
|
227
|
+
end
|
228
|
+
|
229
|
+
private
|
230
|
+
|
231
|
+
def logger
|
232
|
+
@logger ||= TestLogger.new
|
233
|
+
end
|
234
|
+
|
235
|
+
def connection
|
236
|
+
@connection ||= Hurley::HttpCache.new(logger: logger)
|
237
|
+
end
|
238
|
+
end
|