webmock 0.9.1 → 1.0.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.
- data/CHANGELOG.md +47 -2
- data/README.md +68 -6
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/lib/webmock.rb +2 -2
- data/lib/webmock/adapters/rspec.rb +1 -1
- data/lib/webmock/adapters/rspec/matchers.rb +2 -2
- data/lib/webmock/adapters/rspec/{request_profile_matcher.rb → request_pattern_matcher.rb} +5 -5
- data/lib/webmock/adapters/rspec/webmock_matcher.rb +2 -2
- data/lib/webmock/config.rb +2 -1
- data/lib/webmock/http_lib_adapters/httpclient.rb +5 -4
- data/lib/webmock/http_lib_adapters/net_http.rb +5 -3
- data/lib/webmock/http_lib_adapters/patron.rb +82 -0
- data/lib/webmock/request_execution_verifier.rb +8 -8
- data/lib/webmock/request_pattern.rb +130 -0
- data/lib/webmock/request_registry.rb +4 -9
- data/lib/webmock/request_signature.rb +18 -37
- data/lib/webmock/request_stub.rb +17 -6
- data/lib/webmock/response.rb +87 -31
- data/lib/webmock/util/headers.rb +5 -0
- data/lib/webmock/webmock.rb +10 -6
- data/spec/httpclient_spec.rb +0 -1
- data/spec/httpclient_spec_helper.rb +11 -1
- data/spec/net_http_spec.rb +8 -1
- data/spec/net_http_spec_helper.rb +11 -1
- data/spec/patron_spec.rb +83 -0
- data/spec/patron_spec_helper.rb +44 -0
- data/spec/request_execution_verifier_spec.rb +8 -8
- data/spec/request_pattern_spec.rb +243 -0
- data/spec/request_registry_spec.rb +34 -19
- data/spec/request_signature_spec.rb +23 -191
- data/spec/request_stub_spec.rb +32 -11
- data/spec/response_spec.rb +98 -5
- data/spec/spec_helper.rb +8 -16
- data/spec/webmock_spec.rb +154 -49
- data/webmock.gemspec +14 -7
- metadata +21 -7
- data/lib/webmock/request.rb +0 -29
- data/lib/webmock/request_profile.rb +0 -50
- data/spec/request_profile_spec.rb +0 -68
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,50 @@
|
|
1
1
|
#Changelog
|
2
2
|
|
3
|
+
## 1.0.0
|
4
|
+
|
5
|
+
* Added support for [Patron](http://toland.github.com/patron/)
|
6
|
+
|
7
|
+
* Responses dynamically evaluated from block (idea and implementation by Tom Ward)
|
8
|
+
|
9
|
+
stub_request(:any, 'www.example.net').
|
10
|
+
to_return { |request| {:body => request.body} }
|
11
|
+
|
12
|
+
RestClient.post('www.example.net', 'abc') # ===> "abc\n"
|
13
|
+
|
14
|
+
* Responses dynamically evaluated from lambda (idea and implementation by Tom Ward)
|
15
|
+
|
16
|
+
stub_request(:any, 'www.example.net').
|
17
|
+
to_return(lambda { |request| {:body => request.body} })
|
18
|
+
|
19
|
+
RestClient.post('www.example.net', 'abc') # ===> "abc\n"
|
20
|
+
|
21
|
+
* Response with custom status message
|
22
|
+
|
23
|
+
stub_request(:any, "www.example.com").to_return(:status => [500, "Internal Server Error"])
|
24
|
+
|
25
|
+
req = Net::HTTP::Get.new("/")
|
26
|
+
Net::HTTP.start("www.example.com") { |http| http.request(req) }.message # ===> "Internal Server Error"
|
27
|
+
|
28
|
+
* Raising timeout errors (suggested by Jeffrey Jones) (compatibility with Ruby 1.8.6 by Mack Earnhardt)
|
29
|
+
|
30
|
+
stub_request(:any, 'www.example.net').to_timeout
|
31
|
+
|
32
|
+
RestClient.post('www.example.net', 'abc') # ===> RestClient::RequestTimeout
|
33
|
+
|
34
|
+
* External requests can be disabled while allowing localhost (idea and implementation by Mack Earnhardt)
|
35
|
+
|
36
|
+
WebMock.disable_net_connect!(:allow_localhost => true)
|
37
|
+
|
38
|
+
Net::HTTP.get('www.something.com', '/') # ===> Failure
|
39
|
+
|
40
|
+
Net::HTTP.get('localhost:9887', '/') # ===> Allowed. Perhaps to Selenium?
|
41
|
+
|
42
|
+
|
43
|
+
### Bug fixes
|
44
|
+
|
45
|
+
* Fixed issue where Net::HTTP adapter didn't work for requests with body responding to read (reported by Tekin Suleyman)
|
46
|
+
* Fixed issue where request stub with headers declared as nil was matching requests with non empty headers
|
47
|
+
|
3
48
|
## 0.9.1
|
4
49
|
|
5
50
|
* Fixed issue where response status code was not read from raw (curl -is) responses
|
@@ -14,12 +59,12 @@
|
|
14
59
|
#or
|
15
60
|
assert_requested(:post, "www.example.com") { |req| req.body == "abc" }
|
16
61
|
|
17
|
-
* Matching request body against regular expressions
|
62
|
+
* Matching request body against regular expressions (suggested by Ben Pickles)
|
18
63
|
|
19
64
|
stub_request(:post, "www.example.com").with(:body => /^.*world$/).to_return(:body => "abc")
|
20
65
|
RestClient.post('www.example.com', 'hello world') # ===> "abc\n"
|
21
66
|
|
22
|
-
* Matching request headers against regular expressions
|
67
|
+
* Matching request headers against regular expressions (suggested by Ben Pickles)
|
23
68
|
|
24
69
|
stub_request(:post, "www.example.com").with(:headers => {"Content-Type" => /image\/.+/}).to_return(:body => "abc")
|
25
70
|
RestClient.post('www.example.com', '', {'Content-Type' => 'image/png'}) # ===> "abc\n"
|
data/README.md
CHANGED
@@ -14,6 +14,7 @@ Features
|
|
14
14
|
* Support for Test::Unit and RSpec (and can be easily extended to other frameworks)
|
15
15
|
* Support for Net::HTTP and other http libraries based on Net::HTTP (i.e RightHttpConnection, rest-client, HTTParty)
|
16
16
|
* Support for HTTPClient library (both sync and async requests)
|
17
|
+
* Support for Patron library
|
17
18
|
* Easy to extend to other HTTP libraries
|
18
19
|
|
19
20
|
Installation
|
@@ -113,7 +114,7 @@ You can also use WebMock without RSpec or Test::Unit support:
|
|
113
114
|
|
114
115
|
Net::HTTP.get("www.example.com", '/') # ===> "abc"
|
115
116
|
|
116
|
-
###
|
117
|
+
### Response with body specified as IO object
|
117
118
|
|
118
119
|
File.open('/tmp/response_body.txt', 'w') { |f| f.puts 'abc' }
|
119
120
|
|
@@ -121,6 +122,13 @@ You can also use WebMock without RSpec or Test::Unit support:
|
|
121
122
|
|
122
123
|
Net::HTTP.get('www.example.com', '/') # ===> "abc\n"
|
123
124
|
|
125
|
+
### Response with custom status message
|
126
|
+
|
127
|
+
stub_request(:any, "www.example.com").to_return(:status => [500, "Internal Server Error"])
|
128
|
+
|
129
|
+
req = Net::HTTP::Get.new("/")
|
130
|
+
Net::HTTP.start("www.example.com") { |http| http.request(req) }.message # ===> "Internal Server Error"
|
131
|
+
|
124
132
|
### Replaying raw responses recorded with `curl -is`
|
125
133
|
|
126
134
|
`curl -is www.example.com > /tmp/example_curl_-is_output.txt`
|
@@ -134,13 +142,39 @@ You can also use WebMock without RSpec or Test::Unit support:
|
|
134
142
|
|
135
143
|
stub_request(:get, "www.example.com").to_return(raw_response_file.read)
|
136
144
|
|
137
|
-
###
|
138
|
-
|
145
|
+
### Responses dynamically evaluated from block
|
146
|
+
|
147
|
+
stub_request(:any, 'www.example.net').
|
148
|
+
to_return { |request| {:body => request.body} }
|
149
|
+
|
150
|
+
RestClient.post('www.example.net', 'abc') # ===> "abc\n"
|
151
|
+
|
152
|
+
### Responses dynamically evaluated from lambda
|
153
|
+
|
154
|
+
stub_request(:any, 'www.example.net').
|
155
|
+
to_return(lambda { |request| {:body => request.body} })
|
156
|
+
|
157
|
+
RestClient.post('www.example.net', 'abc') # ===> "abc\n"
|
158
|
+
|
159
|
+
### Responses with dynamically evaluated parts
|
160
|
+
|
139
161
|
stub_request(:any, 'www.example.net').
|
140
162
|
to_return(:body => lambda { |request| request.body })
|
141
163
|
|
142
164
|
RestClient.post('www.example.net', 'abc') # ===> "abc\n"
|
143
165
|
|
166
|
+
### Raising errors
|
167
|
+
|
168
|
+
stub_request(:any, 'www.example.net').to_raise(StandardError)
|
169
|
+
|
170
|
+
RestClient.post('www.example.net', 'abc') # ===> StandardError
|
171
|
+
|
172
|
+
### Raising timeout errors
|
173
|
+
|
174
|
+
stub_request(:any, 'www.example.net').to_timeout
|
175
|
+
|
176
|
+
RestClient.post('www.example.net', 'abc') # ===> RestClient::RequestTimeout
|
177
|
+
|
144
178
|
### Multiple responses for repeated requests
|
145
179
|
|
146
180
|
stub_request(:get, "www.example.com").to_return({:body => "abc"}, {:body => "def"})
|
@@ -151,10 +185,10 @@ You can also use WebMock without RSpec or Test::Unit support:
|
|
151
185
|
|
152
186
|
Net::HTTP.get('www.example.com', '/') # ===> "def\n"
|
153
187
|
|
154
|
-
### Multiple responses using chained `to_return()
|
188
|
+
### Multiple responses using chained `to_return()`, `to_raise()` or `to_timeout` declarations
|
155
189
|
|
156
190
|
stub_request(:get, "www.example.com").
|
157
|
-
to_return({:body => "abc"}).then. #then() just
|
191
|
+
to_return({:body => "abc"}).then. #then() is just a syntactic sugar
|
158
192
|
to_return({:body => "def"}).then.
|
159
193
|
to_raise(MyException)
|
160
194
|
Net::HTTP.get('www.example.com', '/') # ===> "abc\n"
|
@@ -186,6 +220,13 @@ You can also use WebMock without RSpec or Test::Unit support:
|
|
186
220
|
|
187
221
|
Net::HTTP.get('www.something.com', '/') # ===> Failure
|
188
222
|
|
223
|
+
### External requests can be disabled while allowing localhost
|
224
|
+
|
225
|
+
WebMock.disable_net_connect!(:allow_localhost => true)
|
226
|
+
|
227
|
+
Net::HTTP.get('www.something.com', '/') # ===> Failure
|
228
|
+
|
229
|
+
Net::HTTP.get('localhost:9887', '/') # ===> Allowed. Perhaps to Selenium?
|
189
230
|
|
190
231
|
## Setting Expectations
|
191
232
|
|
@@ -356,10 +397,31 @@ I'm particularly interested in how the DSL could be improved.
|
|
356
397
|
|
357
398
|
## Credits
|
358
399
|
|
400
|
+
The initial lines of this project were written during New Bamboo [Hack Day](http://blog.new-bamboo.co.uk/2009/11/13/hackday-results)
|
359
401
|
Thanks to my fellow [Bambinos](http://new-bamboo.co.uk/) for all the great suggestions!
|
360
402
|
|
403
|
+
People who submitted patches and new features or suggested improvements. Many thanks to these people:
|
404
|
+
|
405
|
+
* Ben Pickles
|
406
|
+
* Mark Evans
|
407
|
+
* Ivan Vega
|
408
|
+
* Piotr Usewicz
|
409
|
+
* Nick Plante
|
410
|
+
* Nick Quaranto
|
411
|
+
* Diego E. "Flameeyes" Pettenò
|
412
|
+
* Niels Meersschaert
|
413
|
+
* Mack Earnhardt
|
414
|
+
* Arvicco
|
415
|
+
* Sergio Gil
|
416
|
+
* Jeffrey Jones
|
417
|
+
* Tekin Suleyman
|
418
|
+
* Tom Ward
|
419
|
+
* Nadim Bitar
|
420
|
+
|
421
|
+
## Background
|
422
|
+
|
361
423
|
Thank you Fakeweb! This library was inspired by [FakeWeb](fakeweb.rubyforge.org).
|
362
|
-
I
|
424
|
+
I imported some solutions from that project to WebMock. I also copied some code i.e Net:HTTP adapter.
|
363
425
|
Fakeweb architecture unfortunately didn't allow me to extend it easily with the features I needed.
|
364
426
|
I also preferred some things to work differently i.e request stub precedence.
|
365
427
|
|
data/Rakefile
CHANGED
@@ -13,7 +13,7 @@ begin
|
|
13
13
|
gem.add_dependency "addressable", ">= 2.1.1"
|
14
14
|
gem.add_development_dependency "rspec", ">= 1.2.9"
|
15
15
|
gem.add_development_dependency "httpclient", ">= 2.1.5.2"
|
16
|
-
|
16
|
+
gem.add_development_dependency "patron", ">= 0.4.5" unless RUBY_PLATFORM =~ /java/
|
17
17
|
end
|
18
18
|
Jeweler::GemcutterTasks.new
|
19
19
|
rescue LoadError
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
1.0.0
|
data/lib/webmock.rb
CHANGED
@@ -4,6 +4,7 @@ require 'addressable/uri'
|
|
4
4
|
|
5
5
|
require 'webmock/http_lib_adapters/net_http'
|
6
6
|
require 'webmock/http_lib_adapters/httpclient'
|
7
|
+
require 'webmock/http_lib_adapters/patron'
|
7
8
|
|
8
9
|
require 'webmock/errors'
|
9
10
|
|
@@ -11,8 +12,7 @@ require 'webmock/util/uri'
|
|
11
12
|
require 'webmock/util/headers'
|
12
13
|
require 'webmock/util/hash_counter'
|
13
14
|
|
14
|
-
require 'webmock/
|
15
|
-
require 'webmock/request_profile'
|
15
|
+
require 'webmock/request_pattern'
|
16
16
|
require 'webmock/request_signature'
|
17
17
|
require 'webmock/responses_sequence'
|
18
18
|
require 'webmock/request_stub'
|
@@ -1,11 +1,11 @@
|
|
1
1
|
module WebMock
|
2
2
|
module Matchers
|
3
3
|
def have_been_made
|
4
|
-
WebMock::
|
4
|
+
WebMock::RequestPatternMatcher.new
|
5
5
|
end
|
6
6
|
|
7
7
|
def have_not_been_made
|
8
|
-
WebMock::
|
8
|
+
WebMock::RequestPatternMatcher.new.times(0)
|
9
9
|
end
|
10
10
|
|
11
11
|
def have_requested(method, uri)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module WebMock
|
2
|
-
class
|
2
|
+
class RequestPatternMatcher
|
3
3
|
|
4
4
|
def initialize
|
5
5
|
@request_execution_verifier = RequestExecutionVerifier.new
|
@@ -20,13 +20,13 @@ module WebMock
|
|
20
20
|
self
|
21
21
|
end
|
22
22
|
|
23
|
-
def matches?(
|
24
|
-
@request_execution_verifier.
|
23
|
+
def matches?(request_pattern)
|
24
|
+
@request_execution_verifier.request_pattern = request_pattern
|
25
25
|
@request_execution_verifier.matches?
|
26
26
|
end
|
27
27
|
|
28
|
-
def does_not_match?(
|
29
|
-
@request_execution_verifier.
|
28
|
+
def does_not_match?(request_pattern)
|
29
|
+
@request_execution_verifier.request_pattern = request_pattern
|
30
30
|
@request_execution_verifier.does_not_match?
|
31
31
|
end
|
32
32
|
|
@@ -3,7 +3,7 @@ module WebMock
|
|
3
3
|
|
4
4
|
def initialize(method, uri)
|
5
5
|
@request_execution_verifier = RequestExecutionVerifier.new
|
6
|
-
@request_execution_verifier.
|
6
|
+
@request_execution_verifier.request_pattern = RequestPattern.new(method, uri)
|
7
7
|
end
|
8
8
|
|
9
9
|
def once
|
@@ -17,7 +17,7 @@ module WebMock
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def with(options = {}, &block)
|
20
|
-
@request_execution_verifier.
|
20
|
+
@request_execution_verifier.request_pattern.with(options, &block)
|
21
21
|
self
|
22
22
|
end
|
23
23
|
|
data/lib/webmock/config.rb
CHANGED
@@ -19,7 +19,7 @@ if defined?(HTTPClient)
|
|
19
19
|
webmock_response = WebMock.response_for_request(request_signature)
|
20
20
|
response = build_httpclient_response(webmock_response, stream, &block)
|
21
21
|
conn.push(response)
|
22
|
-
elsif WebMock.net_connect_allowed?
|
22
|
+
elsif WebMock.net_connect_allowed?(req.header.request_uri)
|
23
23
|
if stream
|
24
24
|
do_get_stream_without_webmock(req, proxy, conn, &block)
|
25
25
|
else
|
@@ -35,7 +35,7 @@ if defined?(HTTPClient)
|
|
35
35
|
req = create_request(method, uri, query, body, extheader)
|
36
36
|
request_signature = build_request_signature(req)
|
37
37
|
|
38
|
-
if WebMock.registered_request?(request_signature) || WebMock.net_connect_allowed?
|
38
|
+
if WebMock.registered_request?(request_signature) || WebMock.net_connect_allowed?(uri)
|
39
39
|
do_request_async_without_webmock(method, uri, query, body, extheader)
|
40
40
|
else
|
41
41
|
message = "Real HTTP connections are disabled. Unregistered request: #{request_signature}"
|
@@ -55,10 +55,11 @@ if defined?(HTTPClient)
|
|
55
55
|
def build_httpclient_response(webmock_response, stream = false, &block)
|
56
56
|
body = stream ? StringIO.new(webmock_response.body) : webmock_response.body
|
57
57
|
response = HTTP::Message.new_response(body)
|
58
|
-
response.header.init_response(webmock_response.status)
|
59
|
-
|
58
|
+
response.header.init_response(webmock_response.status[0])
|
59
|
+
response.reason=webmock_response.status[1]
|
60
60
|
webmock_response.headers.to_a.each { |name, value| response.header.set(name, value) }
|
61
61
|
|
62
|
+
raise HTTPClient::TimeoutError if webmock_response.should_timeout
|
62
63
|
webmock_response.raise_error_if_any
|
63
64
|
|
64
65
|
block.call(nil, body) if block
|
@@ -91,9 +91,9 @@ module Net #:nodoc: all
|
|
91
91
|
@socket = Net::HTTP.socket_type.new
|
92
92
|
webmock_response = WebMock.response_for_request(request_signature)
|
93
93
|
build_net_http_response(webmock_response, &block)
|
94
|
-
elsif WebMock.net_connect_allowed?
|
94
|
+
elsif WebMock.net_connect_allowed?(uri)
|
95
95
|
connect_without_webmock
|
96
|
-
request_without_webmock(request,
|
96
|
+
request_without_webmock(request, nil, &block)
|
97
97
|
else
|
98
98
|
message = "Real HTTP connections are disabled. Unregistered request: #{request_signature}"
|
99
99
|
WebMock.assertion_failure(message)
|
@@ -114,7 +114,7 @@ module Net #:nodoc: all
|
|
114
114
|
alias_method :connect, :connect_with_webmock
|
115
115
|
|
116
116
|
def build_net_http_response(webmock_response, &block)
|
117
|
-
response = Net::HTTPResponse.send(:response_class, webmock_response.status.to_s).new("1.0", webmock_response.status.to_s,
|
117
|
+
response = Net::HTTPResponse.send(:response_class, webmock_response.status[0].to_s).new("1.0", webmock_response.status[0].to_s, webmock_response.status[1])
|
118
118
|
response.instance_variable_set(:@body, webmock_response.body)
|
119
119
|
webmock_response.headers.to_a.each { |name, value| response[name] = value }
|
120
120
|
|
@@ -122,6 +122,8 @@ module Net #:nodoc: all
|
|
122
122
|
|
123
123
|
response.extend StubResponse
|
124
124
|
|
125
|
+
raise Timeout::Error, "execution expired" if webmock_response.should_timeout
|
126
|
+
|
125
127
|
webmock_response.raise_error_if_any
|
126
128
|
|
127
129
|
yield response if block_given?
|
@@ -0,0 +1,82 @@
|
|
1
|
+
if defined?(Patron)
|
2
|
+
|
3
|
+
module Patron
|
4
|
+
class Session
|
5
|
+
|
6
|
+
def handle_request_with_webmock(req)
|
7
|
+
request_signature = build_request_signature(req)
|
8
|
+
|
9
|
+
WebMock::RequestRegistry.instance.requested_signatures.put(request_signature)
|
10
|
+
|
11
|
+
if WebMock.registered_request?(request_signature)
|
12
|
+
webmock_response = WebMock.response_for_request(request_signature)
|
13
|
+
handle_file_name(req, webmock_response)
|
14
|
+
build_patron_response(webmock_response)
|
15
|
+
elsif WebMock.net_connect_allowed?(req.url)
|
16
|
+
handle_request_without_webmock(req)
|
17
|
+
else
|
18
|
+
message = "Real HTTP connections are disabled. Unregistered request: #{request_signature}"
|
19
|
+
WebMock.assertion_failure(message)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
alias_method :handle_request_without_webmock, :handle_request
|
24
|
+
alias_method :handle_request, :handle_request_with_webmock
|
25
|
+
|
26
|
+
|
27
|
+
|
28
|
+
def handle_file_name(req, webmock_response)
|
29
|
+
if req.action == :get && req.file_name
|
30
|
+
begin
|
31
|
+
File.open(req.file_name, "w") do |f|
|
32
|
+
f.write webmock_response.body
|
33
|
+
end
|
34
|
+
rescue Errno::EACCES
|
35
|
+
raise ArgumentError.new("Unable to open specified file.")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def build_request_signature(req)
|
41
|
+
uri = Addressable::URI.heuristic_parse(req.url)
|
42
|
+
uri.path = uri.normalized_path.gsub("[^:]//","/")
|
43
|
+
uri.user = req.username
|
44
|
+
uri.password = req.password
|
45
|
+
|
46
|
+
if [:put, :post].include?(req.action)
|
47
|
+
if req.file_name
|
48
|
+
if !File.exist?(req.file_name) || !File.readable?(req.file_name)
|
49
|
+
raise ArgumentError.new("Unable to open specified file.")
|
50
|
+
end
|
51
|
+
request_body = File.read(req.file_name)
|
52
|
+
elsif req.upload_data
|
53
|
+
request_body = req.upload_data
|
54
|
+
else
|
55
|
+
raise ArgumentError.new("Must provide either data or a filename when doing a PUT or POST")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
request_signature = WebMock::RequestSignature.new(
|
60
|
+
req.action,
|
61
|
+
uri.to_s,
|
62
|
+
:body => request_body,
|
63
|
+
:headers => req.headers
|
64
|
+
)
|
65
|
+
request_signature
|
66
|
+
end
|
67
|
+
|
68
|
+
def build_patron_response(webmock_response)
|
69
|
+
raise Patron::TimeoutError if webmock_response.should_timeout
|
70
|
+
webmock_response.raise_error_if_any
|
71
|
+
res = Patron::Response.new
|
72
|
+
res.instance_variable_set(:@body, webmock_response.body)
|
73
|
+
res.instance_variable_set(:@status, webmock_response.status[0])
|
74
|
+
res.instance_variable_set(:@status_line, webmock_response.status[1])
|
75
|
+
res.instance_variable_set(:@headers, webmock_response.headers)
|
76
|
+
res
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|