right_popen 1.0.11 → 1.0.16

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,91 @@
1
+ #-- -*- mode: ruby; encoding: utf-8 -*-
2
+ # Copyright: Copyright (c) 2011 RightScale, Inc.
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # 'Software'), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ require File.expand_path(File.join(File.dirname(__FILE__), "process"))
25
+ require File.expand_path(File.join(File.dirname(__FILE__), "accumulator"))
26
+
27
+ module RightScale
28
+ module RightPopen
29
+ module Utilities
30
+ module_function
31
+
32
+ SIGNAL_LOOKUP = Signal.list.invert
33
+
34
+ def reason(status)
35
+ if status.exitstatus
36
+ "with exit status #{status.exitstatus}"
37
+ else
38
+ "due to SIG#{SIGNAL_LOOKUP[status.termsig]}"
39
+ end
40
+ end
41
+ private :reason
42
+
43
+ def run(cmd, parameters={})
44
+ status, out, err = run_collecting_output(cmd, parameters)
45
+ unless status.success?
46
+ raise "Command \"#{cmd}\" failed #{reason(status)}: " +
47
+ "stdout #{out}, stderr #{err}"
48
+ end
49
+ [out, err]
50
+ end
51
+
52
+ def run_with_stdin_collecting_output(cmd, input, parameters={})
53
+ out = StringIO.new
54
+ err = StringIO.new
55
+ first = true
56
+ status = run_with_blocks(cmd,
57
+ Proc.new {
58
+ if (first)
59
+ first = false
60
+ input
61
+ else
62
+ nil
63
+ end},
64
+ Proc.new {|s| out.write(s)},
65
+ Proc.new {|s| err.write(s)})
66
+ [status, out.string, err.string]
67
+ end
68
+ alias_method :run_input, :run_with_stdin_collecting_output
69
+
70
+ def run_collecting_output(cmd, parameters={})
71
+ out = StringIO.new
72
+ err = StringIO.new
73
+ status = run_with_blocks(cmd, nil, Proc.new {|s| out.write(s)},
74
+ Proc.new {|s| err.write(s)})
75
+ [status, out.string, err.string]
76
+ end
77
+ alias_method :spawn, :run_collecting_output
78
+
79
+ def run_with_blocks(cmd, stdin_block, stdout_block, stderr_block, parameters={})
80
+ process = Process.new(parameters)
81
+ process.fork(cmd)
82
+ process.wait_for_exec
83
+ a = Accumulator.new(process,
84
+ [process.stdout, process.stderr], [stdout_block, stderr_block],
85
+ [process.stdin], [stdin_block])
86
+ a.run_to_completion
87
+ process.status[1]
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,28 @@
1
+ #-- -*- mode: ruby; encoding: utf-8 -*-
2
+ # Copyright: Copyright (c) 2011 RightScale, Inc.
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # 'Software'), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ module RightScale
25
+ module RightPopen
26
+ VERSION = "1.0.16"
27
+ end
28
+ end
data/right_popen.gemspec CHANGED
@@ -1,16 +1,15 @@
1
- require 'rubygems'
2
-
3
- def is_windows?
4
- return RUBY_PLATFORM =~ /mswin/
5
- end
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require "right_popen/version"
6
3
 
7
4
  spec = Gem::Specification.new do |spec|
5
+ is_windows = RUBY_PLATFORM =~ /mswin/
6
+
8
7
  spec.name = 'right_popen'
9
- spec.version = '1.0.11'
8
+ spec.version = RightScale::RightPopen::VERSION
10
9
  spec.authors = ['Scott Messier', 'Raphael Simon', 'Graham Hughes']
11
10
  spec.email = 'scott@rightscale.com'
12
11
  spec.homepage = 'https://github.com/rightscale/right_popen'
13
- if is_windows?
12
+ if is_windows
14
13
  spec.platform = 'x86-mswin32-60'
15
14
  else
16
15
  spec.platform = Gem::Platform::RUBY
@@ -29,7 +28,7 @@ of its internal mechanisms. The Linux implementation is valid for any Linux
29
28
  platform but there is also a native implementation for Windows platforms.
30
29
  EOF
31
30
 
32
- if is_windows?
31
+ if is_windows
33
32
  extension_dir = "ext,"
34
33
  else
35
34
  extension_dir = ""
@@ -40,7 +39,7 @@ EOF
40
39
  item.include?("Makefile") || item.include?(".obj") || item.include?(".pdb") || item.include?(".def") || item.include?(".exp") || item.include?(".lib")
41
40
  end
42
41
  candidates = candidates.delete_if do |item|
43
- if is_windows?
42
+ if is_windows
44
43
  item.include?("/linux/")
45
44
  else
46
45
  item.include?("/win32/")
@@ -50,12 +49,10 @@ EOF
50
49
 
51
50
  # Current implementation supports >= 0.12.10
52
51
  spec.add_runtime_dependency(%q<eventmachine>, [">= 0.12.10"])
53
- if is_windows?
52
+ if is_windows
54
53
  spec.add_runtime_dependency(%q<win32-process>, [">= 0.6.1"])
55
54
  end
56
- end
57
-
58
- if $PROGRAM_NAME == __FILE__
59
- Gem.manage_gems if Gem::RubyGemsVersion.to_f < 1.0
60
- Gem::Builder.new(spec).build
55
+ spec.add_development_dependency('rspec', "~> 1.3")
56
+ spec.add_development_dependency('rake', "~> 0.8.7")
57
+ spec.add_development_dependency('flexmock')
61
58
  end
@@ -0,0 +1,28 @@
1
+ #-- -*- mode: ruby; encoding: utf-8 -*-
2
+ # Copyright: Copyright (c) 2011 RightScale, Inc.
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # 'Software'), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ fork {
25
+ sleep 30
26
+ puts "Done!"
27
+ }
28
+ exit 0
@@ -0,0 +1,272 @@
1
+ #-- -*- mode: ruby; encoding: utf-8 -*-
2
+ # Copyright: Copyright (c) 2011 RightScale, Inc.
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # 'Software'), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper'))
25
+
26
+ module RightScale::RightPopen
27
+ describe Accumulator do
28
+ before(:each) do
29
+ @process = flexmock("process")
30
+ @process.should_receive(:pid).and_return(42)
31
+ end
32
+
33
+ describe "#tick" do
34
+ context 'with a live child' do
35
+ before(:each) do
36
+ @process.should_receive(:status).and_return(nil)
37
+ @process.should_receive(:status=)
38
+ @input = flexmock("input")
39
+ @output = flexmock("output")
40
+ @read = flexmock("read")
41
+ @write = flexmock("write")
42
+ end
43
+
44
+ it 'should skip calling select if no pipes are given' do
45
+ a = Accumulator.new(@process, [], [], [], [])
46
+ flexmock(::Process).should_receive(:waitpid2).with(42, ::Process::WNOHANG).once.and_return(nil)
47
+ a.tick.should be_false
48
+ end
49
+
50
+ it 'should just check waitpid if the select times out' do
51
+ a = Accumulator.new(@process, [@input], [@read], [@output], [@write])
52
+ flexmock(::IO).should_receive(:select).with([@input], [@output], nil, 0.1).once.and_return([[], [], []])
53
+ flexmock(::Process).should_receive(:waitpid2).with(42, ::Process::WNOHANG).once.and_return(nil)
54
+ a.tick.should be_false
55
+ end
56
+
57
+ it 'should use the timeout value in the select' do
58
+ value = flexmock("value")
59
+ a = Accumulator.new(@process, [@input], [@read], [@output], [@write])
60
+ flexmock(::IO).should_receive(:select).with([@input], [@output], nil, value).once.and_return([[], [], []])
61
+ flexmock(::Process).should_receive(:waitpid2).with(42, ::Process::WNOHANG).once.and_return(nil)
62
+ a.tick(value).should be_false
63
+ end
64
+
65
+ it 'should retry the select when seeing Errno::EAGAIN or Errno::EINTR' do
66
+ a = Accumulator.new(@process, [@input], [@read], [@output], [@write])
67
+ flexmock(::IO).should_receive(:select).with([@input], [@output], nil, 0.1).times(3).and_raise(Errno::EAGAIN).and_raise(Errno::EINTR).and_return([[], [], []])
68
+ flexmock(::Process).should_receive(:waitpid2).with(42, ::Process::WNOHANG).once.and_return(nil)
69
+ a.tick.should be_false
70
+ end
71
+
72
+ it 'should read data from the pipe and call the reader if it is ready' do
73
+ value = flexmock("value")
74
+ a = Accumulator.new(@process, [@input], [@read], [@output], [@write])
75
+ flexmock(::IO).should_receive(:select).with([@input], [@output], nil, 0.1).once.and_return([[@input], [], []])
76
+ @input.should_receive(:eof?).once.and_return(false)
77
+ @input.should_receive(:readpartial).with(Accumulator::READ_CHUNK_SIZE).once.and_return(value)
78
+ @read.should_receive(:call).with(value).once
79
+ flexmock(::Process).should_receive(:waitpid2).with(42, ::Process::WNOHANG).once.and_return(nil)
80
+ a.tick.should be_false
81
+ end
82
+
83
+ it 'should read data from the pipe and throw it away if no reader' do
84
+ value = flexmock("value")
85
+ a = Accumulator.new(@process, [@input], [], [@output], [@write])
86
+ flexmock(::IO).should_receive(:select).with([@input], [@output], nil, 0.1).once.and_return([[@input], [], []])
87
+ @input.should_receive(:eof?).once.and_return(false)
88
+ @input.should_receive(:readpartial).with(Accumulator::READ_CHUNK_SIZE).once.and_return(value)
89
+ flexmock(::Process).should_receive(:waitpid2).with(42, ::Process::WNOHANG).once.and_return(nil)
90
+ a.tick.should be_false
91
+ end
92
+
93
+ it 'should call the writer and then write data to the pipe if it is ready' do
94
+ value = flexmock("value")
95
+ a = Accumulator.new(@process, [@input], [@read], [@output], [@write])
96
+ flexmock(::IO).should_receive(:select).with([@input], [@output], nil, 0.1).once.and_return([[], [@output], []])
97
+ @write.should_receive(:call).with().once.and_return(value)
98
+ value.should_receive(:[]).with(30..-1).and_return("")
99
+ @output.should_receive(:write_nonblock).with(value).once.and_return(30)
100
+ flexmock(::Process).should_receive(:waitpid2).with(42, ::Process::WNOHANG).once.and_return(nil)
101
+ a.tick.should be_false
102
+ end
103
+
104
+ it 'should only call the writer when it is stalling' do
105
+ value = flexmock("value")
106
+ other = flexmock("other value")
107
+ a = Accumulator.new(@process, [@input], [@read], [@output], [@write])
108
+ flexmock(::IO).should_receive(:select).with([@input], [@output], nil, 0.1).and_return([[], [@output], []])
109
+ @write.should_receive(:call).with().once.and_return(value)
110
+ value.should_receive(:[]).with(30..-1).and_return(other)
111
+ other.should_receive(:[]).with(20..-1).and_return("")
112
+ @output.should_receive(:write_nonblock).with(value).once.and_return(30)
113
+ @output.should_receive(:write_nonblock).with(other).once.and_return(20)
114
+ flexmock(::Process).should_receive(:waitpid2).with(42, ::Process::WNOHANG).and_return(nil)
115
+ a.tick.should be_false
116
+ a.tick.should be_false
117
+ end
118
+
119
+ it 'should not read data from the pipe any more if EOF has been reached' do
120
+ value = flexmock("value")
121
+ a = Accumulator.new(@process, [@input], [@read], [], [])
122
+ flexmock(::IO).should_receive(:select).with([@input], [], nil, 0.1).once.and_return([[@input], [], []])
123
+ @input.should_receive(:eof?).once.and_return(true)
124
+ @input.should_receive(:close).once
125
+ flexmock(::Process).should_receive(:waitpid2).with(42, ::Process::WNOHANG).twice.and_return(nil)
126
+ a.tick.should be_false
127
+ a.tick.should be_false
128
+ end
129
+
130
+ it 'should not write data to the pipe any more if the caller has no more data' do
131
+ value = flexmock("value")
132
+ a = Accumulator.new(@process, [], [], [@output], [@write])
133
+ flexmock(::IO).should_receive(:select).with([], [@output], nil, 0.1).once.and_return([[], [@output], []])
134
+ @write.should_receive(:call).once.and_return(nil)
135
+ @output.should_receive(:close).once
136
+ flexmock(::Process).should_receive(:waitpid2).with(42, ::Process::WNOHANG).twice.and_return(nil)
137
+ a.tick.should be_false
138
+ a.tick.should be_false
139
+ end
140
+
141
+ it 'should not write data to the pipe any more if the caller is nil' do
142
+ a = Accumulator.new(@process, [], [], [@output], [nil])
143
+ flexmock(::IO).should_receive(:select).with([], [@output], nil, 0.1).once.and_return([[], [@output], []])
144
+ @output.should_receive(:close).once
145
+ flexmock(::Process).should_receive(:waitpid2).with(42, ::Process::WNOHANG).twice.and_return(nil)
146
+ a.tick.should be_false
147
+ a.tick.should be_false
148
+ end
149
+ end
150
+
151
+ it 'should update the status if waitpid is successful' do
152
+ a = Accumulator.new(@process, [], [], [], [])
153
+ status = flexmock("status")
154
+ flexmock(::Process).should_receive(:waitpid2).with(42, ::Process::WNOHANG).once.and_return(status)
155
+ @process.should_receive(:status).twice.and_return(nil).and_return(status)
156
+ @process.should_receive(:status=).with(status).once
157
+ a.tick.should be_true
158
+ end
159
+
160
+ it 'should return true if the process has already been waited on' do
161
+ @process.should_receive(:status).and_return(true)
162
+ a = Accumulator.new(@process, [], [], [], [])
163
+ a.tick.should be_true
164
+ end
165
+ end
166
+
167
+ describe "#number_waiting_on" do
168
+ it 'should return 0 when no pipes are left' do
169
+ a = Accumulator.new(@process, [], [], [], [])
170
+ a.number_waiting_on.should == 0
171
+ end
172
+
173
+ it 'should return 1 when one pipe is left' do
174
+ pipe = flexmock("pipe")
175
+ a = Accumulator.new(@process, [pipe], [nil], [], [])
176
+ a.number_waiting_on.should == 1
177
+ end
178
+
179
+ it 'should return add readers and writers' do
180
+ pipe = flexmock("pipe")
181
+ a = Accumulator.new(@process, [pipe, pipe, pipe], [], [pipe], [])
182
+ a.number_waiting_on.should == 4
183
+ end
184
+
185
+ it 'should transition from 0 to 1 as pipes are removed' do
186
+ pipe = flexmock("pipe")
187
+ read = flexmock("read")
188
+ @process.should_receive(:status).and_return(nil)
189
+ @process.should_receive(:status=)
190
+ a = Accumulator.new(@process, [pipe], [read], [], [])
191
+ flexmock(::IO).should_receive(:select).with([pipe], [], nil, 0.1).once.and_return([[pipe], [], []])
192
+ pipe.should_receive(:eof?).and_return(true)
193
+ pipe.should_receive(:close)
194
+ flexmock(::Process).should_receive(:waitpid2).with(42, ::Process::WNOHANG).once.and_return(nil)
195
+ a.number_waiting_on.should == 1
196
+ a.tick.should be_false
197
+ a.number_waiting_on.should == 0
198
+ end
199
+ end
200
+
201
+ describe "#cleanup" do
202
+ it 'should do nothing if no pipes are left open and the process is reaped' do
203
+ @process.should_receive(:status).and_return(true)
204
+ a = Accumulator.new(@process, [], [], [], [])
205
+ flexmock(::Process).should_receive(:waitpid2).never
206
+ a.cleanup
207
+ end
208
+
209
+ it 'should just call waitpid if no pipes are left open' do
210
+ value = flexmock("value")
211
+ a = Accumulator.new(@process, [], [], [], [])
212
+ @process.should_receive(:status).and_return(nil)
213
+ flexmock(::Process).should_receive(:waitpid2).with(42).once.and_return(value)
214
+ @process.should_receive(:status=).with(value).once
215
+ a.cleanup
216
+ end
217
+
218
+ it 'should close all open pipes' do
219
+ a, b, c = flexmock("a"), flexmock("b"), flexmock("c")
220
+ acc = Accumulator.new(@process, [a, b], [], [c], [])
221
+ @process.should_receive(:status).and_return(true)
222
+ [a, b].each {|fdes| fdes.should_receive(:close).with().once }
223
+ [a, b].each {|fdes| fdes.should_receive(:closed?).and_return(false) }
224
+ [c].each {|fdes| fdes.should_receive(:closed?).and_return(true) }
225
+ flexmock(::Process).should_receive(:waitpid2).never
226
+ acc.cleanup
227
+ end
228
+
229
+ it 'should close all open pipes and reap zombies if needed' do
230
+ value = flexmock("value")
231
+ a, b, c = flexmock("a"), flexmock("b"), flexmock("c")
232
+ acc = Accumulator.new(@process, [b, c], [], [a], [])
233
+ @process.should_receive(:status).and_return(nil)
234
+ [a, b].each {|fdes| fdes.should_receive(:close).with().once }
235
+ [a, b].each {|fdes| fdes.should_receive(:closed?).and_return(false) }
236
+ [c].each {|fdes| fdes.should_receive(:closed?).and_return(true) }
237
+ flexmock(::Process).should_receive(:waitpid2).with(42).once.and_return(value)
238
+ @process.should_receive(:status=).with(value).once
239
+ acc.cleanup
240
+ end
241
+ end
242
+
243
+ describe "#run_to_completion" do
244
+ it 'should run ticks until it is true' do
245
+ value = flexmock("value")
246
+ acc = flexmock(Accumulator.new(@process, [], [], [], []))
247
+ acc.should_receive(:tick).with(value).times(3).and_return(false).and_return(false).and_return(true)
248
+ acc.should_receive(:cleanup).once
249
+ acc.should_receive(:number_waiting_on).and_return(1)
250
+ acc.run_to_completion(value)
251
+ end
252
+
253
+ it 'should abort the loop early if there are no remaining pipes' do
254
+ value = flexmock("value")
255
+ acc = flexmock(Accumulator.new(@process, [], [], [], []))
256
+ acc.should_receive(:tick).with(value).twice.and_return(false)
257
+ acc.should_receive(:cleanup).once
258
+ acc.should_receive(:number_waiting_on).and_return(1).and_return(0)
259
+ acc.run_to_completion(value)
260
+ end
261
+
262
+ it 'should abort the loop after one iteration if there never were any pipes' do
263
+ value = flexmock("value")
264
+ acc = flexmock(Accumulator.new(@process, [], [], [], []))
265
+ acc.should_receive(:tick).with(value).once.and_return(false)
266
+ acc.should_receive(:cleanup).once
267
+ acc.should_receive(:number_waiting_on).and_return(0)
268
+ acc.run_to_completion(value)
269
+ end
270
+ end
271
+ end
272
+ end