kumogata 0.3.13 → 0.3.14
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/README.md +2 -2
- data/lib/kumogata.rb +2 -0
- data/lib/kumogata/post_processing.rb +80 -14
- data/lib/kumogata/string_stream.rb +39 -0
- data/lib/kumogata/version.rb +1 -1
- data/spec/kumogata_create_spec.rb +15 -5
- data/spec/kumogata_update_spec.rb +15 -5
- data/spec/string_stream_spec.rb +123 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
NWY1ZGU1ZTExOTU3MWMyMmYwNGViNGVkNTc0ZGI3OTkxMzI2NTRkNA==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
NjE4MjUwNWQ5ZjQ0Njg5YzUzNDYxZDQwOTIzNTdjZmFlZGViM2RjNQ==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
YjE1YzNkYWJmMTcwZTNiMjdmMjZhMWUwNDc2ZDJkOWQzYjgwOTRlMDY3NTM4
|
10
|
+
NWQzZTYxM2U5OTY1NjgwMGE3MWI5YjRmYmRmZjI3OGM4MTJlMzdhOTJkZmFm
|
11
|
+
OGIzNTc3NTliMjYzMjVlNzYxOWE5Y2Y2YTJlMDZlYjNjMDVjMDc=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
Y2ZiMzE5MmM3ZDU0MGJlZjJjNGVlYjRhOWI3MGNlZmU2YTFlOTc3NTU5YzM2
|
14
|
+
N2JiMzk1ZTJmNzllNDM5NzMyOWIzZmQ3NWU2NWQ1Mzk3NDMzMWY3YTQxZjMz
|
15
|
+
ZGJiNDNmNDE1OWMyNDU2MDU1Yzg5Y2Q4NDUxMTYxZjM4MGNhYzY=
|
data/README.md
CHANGED
@@ -5,8 +5,8 @@
|
|
5
5
|
|
6
6
|
Kumogata is a tool for [AWS CloudFormation](https://aws.amazon.com/cloudformation/).
|
7
7
|
|
8
|
-
[![Gem Version](https://badge.fury.io/rb/kumogata.png?
|
9
|
-
[![Build Status](https://drone.io/github.com/winebarrel/kumogata/status.png?
|
8
|
+
[![Gem Version](https://badge.fury.io/rb/kumogata.png?201403121243)](http://badge.fury.io/rb/kumogata)
|
9
|
+
[![Build Status](https://drone.io/github.com/winebarrel/kumogata/status.png?201403121243)](https://drone.io/github.com/winebarrel/kumogata/latest)
|
10
10
|
|
11
11
|
It can define a template in Ruby DSL, such as:
|
12
12
|
|
data/lib/kumogata.rb
CHANGED
@@ -19,6 +19,7 @@ require 'set'
|
|
19
19
|
require 'singleton'
|
20
20
|
require 'strscan'
|
21
21
|
require 'term/ansicolor'
|
22
|
+
require 'thread'
|
22
23
|
require 'uuidtools'
|
23
24
|
|
24
25
|
require 'kumogata/argument_parser'
|
@@ -29,4 +30,5 @@ require 'kumogata/ext/json_ext'
|
|
29
30
|
require 'kumogata/ext/string_ext'
|
30
31
|
require 'kumogata/logger'
|
31
32
|
require 'kumogata/post_processing'
|
33
|
+
require 'kumogata/string_stream'
|
32
34
|
require 'kumogata/utils'
|
@@ -135,13 +135,22 @@ class Kumogata::PostProcessing
|
|
135
135
|
connect_tries = (ssh['connect_tries'] || 36).to_i
|
136
136
|
retry_interval = (ssh['retry_interval'] || 5).to_i
|
137
137
|
|
138
|
+
stderr_orig = nil
|
139
|
+
|
138
140
|
begin
|
139
|
-
|
140
|
-
|
141
|
+
stderr_orig = STDERR.dup
|
142
|
+
STDERR.reopen('/dev/null', 'w')
|
143
|
+
|
144
|
+
begin
|
145
|
+
retryable(:tries => connect_tries, :on => Net::SSH::Disconnect, :sleep => retry_interval) do
|
146
|
+
Net::SSH.start(*args) {|ssh| ssh_exec!(ssh, command) }
|
147
|
+
end
|
148
|
+
rescue Net::SSH::HostKeyMismatch => e
|
149
|
+
e.remember_host!
|
150
|
+
retry
|
141
151
|
end
|
142
|
-
|
143
|
-
|
144
|
-
retry
|
152
|
+
ensure
|
153
|
+
STDERR.reopen(stderr_orig)
|
145
154
|
end
|
146
155
|
end
|
147
156
|
|
@@ -151,6 +160,9 @@ class Kumogata::PostProcessing
|
|
151
160
|
exit_code = nil
|
152
161
|
#exit_signal = nil
|
153
162
|
|
163
|
+
stdout_stream = create_stdout_stream
|
164
|
+
stderr_stream = create_stderr_stream
|
165
|
+
|
154
166
|
ssh.open_channel do |channel|
|
155
167
|
channel.exec(command) do |ch, success|
|
156
168
|
unless success
|
@@ -158,10 +170,12 @@ class Kumogata::PostProcessing
|
|
158
170
|
end
|
159
171
|
|
160
172
|
channel.on_data do |ch, data|
|
173
|
+
stdout_stream.push data
|
161
174
|
stdout_data << data
|
162
175
|
end
|
163
176
|
|
164
177
|
channel.on_extended_data do |ch, type, data|
|
178
|
+
stderr_stream.push data
|
165
179
|
stderr_data << data
|
166
180
|
end
|
167
181
|
|
@@ -177,13 +191,57 @@ class Kumogata::PostProcessing
|
|
177
191
|
|
178
192
|
ssh.loop
|
179
193
|
|
194
|
+
stdout_stream.close
|
195
|
+
stderr_stream.close
|
196
|
+
|
180
197
|
#[stdout_data, stderr_data, exit_code, exit_signal]
|
181
198
|
[stdout_data, stderr_data, exit_code]
|
182
199
|
end
|
183
200
|
|
184
201
|
def run_shell_command(command, outputs)
|
185
202
|
command = evaluate_command_template(command, outputs)
|
186
|
-
|
203
|
+
|
204
|
+
stdout_data = ''
|
205
|
+
stderr_data = ''
|
206
|
+
exit_code = nil
|
207
|
+
|
208
|
+
Open3.popen3(command) do |stdin, stdout, stderr, wait_thr|
|
209
|
+
mutex = Mutex.new
|
210
|
+
|
211
|
+
th_out = Thread.start do
|
212
|
+
stdout_stream = create_stdout_stream
|
213
|
+
|
214
|
+
stdout.each_line do |line|
|
215
|
+
mutex.synchronize do
|
216
|
+
stdout_stream.push line
|
217
|
+
end
|
218
|
+
|
219
|
+
stdout_data << line
|
220
|
+
end
|
221
|
+
|
222
|
+
stdout_stream.close
|
223
|
+
end
|
224
|
+
|
225
|
+
th_err = Thread.start do
|
226
|
+
stderr_stream = create_stderr_stream
|
227
|
+
|
228
|
+
stderr.each_line do |line|
|
229
|
+
mutex.synchronize do
|
230
|
+
stderr_stream.push line
|
231
|
+
end
|
232
|
+
stderr_data << line
|
233
|
+
end
|
234
|
+
|
235
|
+
stderr_stream.close
|
236
|
+
end
|
237
|
+
|
238
|
+
th_out.join
|
239
|
+
th_err.join
|
240
|
+
exit_code = wait_thr.value
|
241
|
+
end
|
242
|
+
|
243
|
+
#[stdout_data, stderr_data, exit_code, exit_signal]
|
244
|
+
[stdout_data, stderr_data, exit_code]
|
187
245
|
end
|
188
246
|
|
189
247
|
def validate_command_template(name, command, outputs)
|
@@ -232,17 +290,25 @@ Command: #{name.intense_blue}
|
|
232
290
|
EOS
|
233
291
|
end
|
234
292
|
|
235
|
-
def
|
293
|
+
def create_stdout_stream
|
294
|
+
Kumogata::StringStream.new do |line|
|
295
|
+
puts '1> '.intense_green + line
|
296
|
+
$stdout.flush
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
def create_stderr_stream
|
301
|
+
Kumogata::StringStream.new do |line|
|
302
|
+
puts '2> '.intense_red + line
|
303
|
+
$stdout.flush
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def print_command_result(out, err, status) # XXX:
|
236
308
|
status = status.to_i
|
237
|
-
dspout = (out || '').lines.map {|i| "1> ".intense_green + i }.join.chomp
|
238
|
-
dsperr = (err || '').lines.map {|i| "2> ".intense_red + i }.join.chomp
|
239
309
|
|
240
310
|
puts <<-EOS
|
241
|
-
Status: #{status.zero? ? status : status.to_s.red}
|
242
|
-
dspout.empty? ? '' : ("\n---\n" + dspout)
|
243
|
-
}#{
|
244
|
-
dsperr.empty? ? '' : ("\n---\n" + dsperr)
|
245
|
-
}
|
311
|
+
Status: #{status.zero? ? status : status.to_s.red}
|
246
312
|
EOS
|
247
313
|
end
|
248
314
|
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class Kumogata::StringStream
|
2
|
+
def initialize(&block)
|
3
|
+
@buf = StringScanner.new('')
|
4
|
+
@block = block
|
5
|
+
|
6
|
+
@fiber = Fiber.new do
|
7
|
+
self.run
|
8
|
+
end
|
9
|
+
|
10
|
+
# Step to `yield`
|
11
|
+
@fiber.resume
|
12
|
+
end
|
13
|
+
|
14
|
+
def run
|
15
|
+
loop do
|
16
|
+
chunk = Fiber.yield
|
17
|
+
break unless chunk
|
18
|
+
|
19
|
+
@buf << chunk.to_s
|
20
|
+
self.each_line
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def each_line
|
25
|
+
while (line = @buf.scan_until(/(\r\n|\r|\n)/))
|
26
|
+
@block.call(line.chomp)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def push(chunk)
|
31
|
+
@fiber.resume(chunk)
|
32
|
+
end
|
33
|
+
|
34
|
+
def close
|
35
|
+
self.each_line
|
36
|
+
@block.call(@buf.rest) if @buf.rest?
|
37
|
+
@fiber.resume
|
38
|
+
end
|
39
|
+
end
|
data/lib/kumogata/version.rb
CHANGED
@@ -146,9 +146,13 @@ end
|
|
146
146
|
process_status1 = make_double('process_status1') {|obj| obj.should_receive(:to_i).and_return(0) }
|
147
147
|
process_status2 = make_double('process_status2') {|obj| obj.should_receive(:to_i).and_return(0) }
|
148
148
|
|
149
|
-
|
149
|
+
client.instance_variable_get(:@post_processing)
|
150
|
+
.should_receive(:run_shell_command)
|
151
|
+
.with(" echo <%= Key \"AZ\" %>\n echo <%= Key \"Region\" %>\n", {"AZ"=>"ap-northeast-1b", "Region"=>"ap-northeast-1"})
|
150
152
|
.and_return(["ap-northeast-1b\nap-northeast-1\n", "", process_status1])
|
151
|
-
|
153
|
+
client.instance_variable_get(:@post_processing)
|
154
|
+
.should_receive(:run_shell_command)
|
155
|
+
.with(" echo <%= Key \"Region\" %>\n echo <%= Key \"AZ\" %>\n", {"AZ"=>"ap-northeast-1b", "Region"=>"ap-northeast-1"})
|
152
156
|
.and_return(["ap-northeast-1\nap-northeast-1b\n", "", process_status2])
|
153
157
|
|
154
158
|
client.instance_variable_get(:@post_processing)
|
@@ -240,7 +244,9 @@ end
|
|
240
244
|
|
241
245
|
cf.should_receive(:stacks).twice { stacks }
|
242
246
|
|
243
|
-
|
247
|
+
client.instance_variable_get(:@post_processing)
|
248
|
+
.should_receive(:run_ssh_command)
|
249
|
+
.with({"host"=>"<%= Key \"PublicIp\" %>", "user"=>"ec2-user"}, " ls\n", {"PublicIp"=>"127.0.0.1"})
|
244
250
|
.and_return(["file1\nfile2\n", "", 0])
|
245
251
|
|
246
252
|
client.instance_variable_get(:@post_processing)
|
@@ -345,9 +351,13 @@ end
|
|
345
351
|
process_status1 = make_double('process_status1') {|obj| obj.should_receive(:to_i).and_return(0) }
|
346
352
|
process_status2 = make_double('process_status2') {|obj| obj.should_receive(:to_i).and_return(0) }
|
347
353
|
|
348
|
-
|
354
|
+
client.instance_variable_get(:@post_processing)
|
355
|
+
.should_receive(:run_shell_command)
|
356
|
+
.with(" echo <%= Key \"AZ\" %>\n echo <%= Key \"Region\" %>\n", {"AZ"=>"ap-northeast-1b", "Region"=>"ap-northeast-1"})
|
349
357
|
.and_return(["ap-northeast-1b\nap-northeast-1\n", "", process_status1])
|
350
|
-
|
358
|
+
client.instance_variable_get(:@post_processing)
|
359
|
+
.should_receive(:run_shell_command)
|
360
|
+
.with(" echo <%= Key \"Region\" %>\n echo <%= Key \"AZ\" %>\n", {"AZ"=>"ap-northeast-1b", "Region"=>"ap-northeast-1"})
|
351
361
|
.and_return(["ap-northeast-1\nap-northeast-1b\n", "", process_status2])
|
352
362
|
|
353
363
|
client.instance_variable_get(:@post_processing)
|
@@ -141,9 +141,13 @@ end
|
|
141
141
|
process_status1 = make_double('process_status1') {|obj| obj.should_receive(:to_i).and_return(0) }
|
142
142
|
process_status2 = make_double('process_status2') {|obj| obj.should_receive(:to_i).and_return(0) }
|
143
143
|
|
144
|
-
|
144
|
+
client.instance_variable_get(:@post_processing)
|
145
|
+
.should_receive(:run_shell_command)
|
146
|
+
.with(" echo <%= Key \"AZ\" %>\n echo <%= Key \"Region\" %>\n", {"AZ"=>"ap-northeast-1b", "Region"=>"ap-northeast-1"})
|
145
147
|
.and_return(["ap-northeast-1b\nap-northeast-1\n", "", process_status1])
|
146
|
-
|
148
|
+
client.instance_variable_get(:@post_processing)
|
149
|
+
.should_receive(:run_shell_command)
|
150
|
+
.with(" echo <%= Key \"Region\" %>\n echo <%= Key \"AZ\" %>\n", {"AZ"=>"ap-northeast-1b", "Region"=>"ap-northeast-1"})
|
147
151
|
.and_return(["ap-northeast-1\nap-northeast-1b\n", "", process_status2])
|
148
152
|
|
149
153
|
client.instance_variable_get(:@post_processing)
|
@@ -232,7 +236,9 @@ end
|
|
232
236
|
|
233
237
|
cf.should_receive(:stacks) { stacks }
|
234
238
|
|
235
|
-
|
239
|
+
client.instance_variable_get(:@post_processing)
|
240
|
+
.should_receive(:run_ssh_command)
|
241
|
+
.with({"host"=>"<%= Key \"PublicIp\" %>", "user"=>"ec2-user"}, " ls\n", {"PublicIp"=>"127.0.0.1"})
|
236
242
|
.and_return(["file1\nfile2\n", "", 0])
|
237
243
|
|
238
244
|
client.instance_variable_get(:@post_processing)
|
@@ -336,9 +342,13 @@ end
|
|
336
342
|
process_status1 = make_double('process_status1') {|obj| obj.should_receive(:to_i).and_return(0) }
|
337
343
|
process_status2 = make_double('process_status2') {|obj| obj.should_receive(:to_i).and_return(0) }
|
338
344
|
|
339
|
-
|
345
|
+
client.instance_variable_get(:@post_processing)
|
346
|
+
.should_receive(:run_shell_command)
|
347
|
+
.with(" echo <%= Key \"AZ\" %>\n echo <%= Key \"Region\" %>\n", {"AZ"=>"ap-northeast-1b", "Region"=>"ap-northeast-1"})
|
340
348
|
.and_return(["ap-northeast-1b\nap-northeast-1\n", "", process_status1])
|
341
|
-
|
349
|
+
client.instance_variable_get(:@post_processing)
|
350
|
+
.should_receive(:run_shell_command)
|
351
|
+
.with(" echo <%= Key \"Region\" %>\n echo <%= Key \"AZ\" %>\n", {"AZ"=>"ap-northeast-1b", "Region"=>"ap-northeast-1"})
|
342
352
|
.and_return(["ap-northeast-1\nap-northeast-1b\n", "", process_status2])
|
343
353
|
|
344
354
|
client.instance_variable_get(:@post_processing)
|
@@ -0,0 +1,123 @@
|
|
1
|
+
describe Kumogata::StringStream do
|
2
|
+
it 'pass the line ("\n")' do
|
3
|
+
lines = []
|
4
|
+
|
5
|
+
sstream = Kumogata::StringStream.new do |line|
|
6
|
+
lines << line
|
7
|
+
end
|
8
|
+
|
9
|
+
sstream.push("chunk1")
|
10
|
+
sstream.push("chunk2\n")
|
11
|
+
sstream.push("chunk3")
|
12
|
+
sstream.push("chunk4")
|
13
|
+
sstream.push("chunk5\n")
|
14
|
+
sstream.push("\n")
|
15
|
+
sstream.push("\n")
|
16
|
+
sstream.push("chunk6")
|
17
|
+
sstream.push("chunk7")
|
18
|
+
sstream.close
|
19
|
+
|
20
|
+
expect(lines).to eq([
|
21
|
+
"chunk1chunk2",
|
22
|
+
"chunk3chunk4chunk5",
|
23
|
+
"",
|
24
|
+
"",
|
25
|
+
"chunk6chunk7",
|
26
|
+
])
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'pass the line ("\r")' do
|
30
|
+
lines = []
|
31
|
+
|
32
|
+
sstream = Kumogata::StringStream.new do |line|
|
33
|
+
lines << line
|
34
|
+
end
|
35
|
+
|
36
|
+
sstream.push("chunk1")
|
37
|
+
sstream.push("chunk2\r")
|
38
|
+
sstream.push("chunk3")
|
39
|
+
sstream.push("chunk4")
|
40
|
+
sstream.push("chunk5\r")
|
41
|
+
sstream.push("\r")
|
42
|
+
sstream.push("\r")
|
43
|
+
sstream.push("chunk6")
|
44
|
+
sstream.push("chunk7")
|
45
|
+
sstream.close
|
46
|
+
|
47
|
+
expect(lines).to eq([
|
48
|
+
"chunk1chunk2",
|
49
|
+
"chunk3chunk4chunk5",
|
50
|
+
"",
|
51
|
+
"",
|
52
|
+
"chunk6chunk7",
|
53
|
+
])
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'pass the line ("\r\n")' do
|
57
|
+
lines = []
|
58
|
+
|
59
|
+
sstream = Kumogata::StringStream.new do |line|
|
60
|
+
lines << line
|
61
|
+
end
|
62
|
+
|
63
|
+
sstream.push("chunk1")
|
64
|
+
sstream.push("chunk2\r\n")
|
65
|
+
sstream.push("chunk3")
|
66
|
+
sstream.push("chunk4")
|
67
|
+
sstream.push("chunk5\r\n")
|
68
|
+
sstream.push("\r\n")
|
69
|
+
sstream.push("\r\n")
|
70
|
+
sstream.push("chunk6")
|
71
|
+
sstream.push("chunk7")
|
72
|
+
sstream.close
|
73
|
+
|
74
|
+
expect(lines).to eq([
|
75
|
+
"chunk1chunk2",
|
76
|
+
"chunk3chunk4chunk5",
|
77
|
+
"",
|
78
|
+
"",
|
79
|
+
"chunk6chunk7",
|
80
|
+
])
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'pass the line ("\n" / "\r" / "\r\n")' do
|
84
|
+
lines = []
|
85
|
+
|
86
|
+
sstream = Kumogata::StringStream.new do |line|
|
87
|
+
lines << line
|
88
|
+
end
|
89
|
+
|
90
|
+
sstream.push("chunk1")
|
91
|
+
sstream.push("chunk2\n")
|
92
|
+
sstream.push("chunk3")
|
93
|
+
sstream.push("chunk4")
|
94
|
+
sstream.push("chunk5\r")
|
95
|
+
sstream.push("\r\n")
|
96
|
+
sstream.push("\n")
|
97
|
+
sstream.push("chunk6")
|
98
|
+
sstream.push("chunk7")
|
99
|
+
sstream.push("chunk1")
|
100
|
+
sstream.push("chunk2\r")
|
101
|
+
sstream.push("chunk3")
|
102
|
+
sstream.push("chunk4")
|
103
|
+
sstream.push("chunk5\n\r")
|
104
|
+
sstream.push("\n")
|
105
|
+
sstream.push("\r")
|
106
|
+
sstream.push("chunk6")
|
107
|
+
sstream.push("chunk7")
|
108
|
+
sstream.close
|
109
|
+
|
110
|
+
expect(lines).to eq([
|
111
|
+
"chunk1chunk2",
|
112
|
+
"chunk3chunk4chunk5",
|
113
|
+
"",
|
114
|
+
"",
|
115
|
+
"chunk6chunk7chunk1chunk2",
|
116
|
+
"chunk3chunk4chunk5",
|
117
|
+
"",
|
118
|
+
"",
|
119
|
+
"",
|
120
|
+
"chunk6chunk7",
|
121
|
+
])
|
122
|
+
end
|
123
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kumogata
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.14
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Genki Sugawara
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-03-
|
11
|
+
date: 2014-03-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk
|
@@ -231,6 +231,7 @@ files:
|
|
231
231
|
- lib/kumogata/ext/string_ext.rb
|
232
232
|
- lib/kumogata/logger.rb
|
233
233
|
- lib/kumogata/post_processing.rb
|
234
|
+
- lib/kumogata/string_stream.rb
|
234
235
|
- lib/kumogata/utils.rb
|
235
236
|
- lib/kumogata/version.rb
|
236
237
|
- packer/CentOS-6.4-x86_64-with_Updates-Asia_Pacific.json
|
@@ -250,6 +251,7 @@ files:
|
|
250
251
|
- spec/kumogata_update_spec.rb
|
251
252
|
- spec/kumogata_validate_spec.rb
|
252
253
|
- spec/spec_helper.rb
|
254
|
+
- spec/string_stream_spec.rb
|
253
255
|
homepage: https://github.com/winebarrel/kumogata
|
254
256
|
licenses:
|
255
257
|
- MIT
|
@@ -290,3 +292,4 @@ test_files:
|
|
290
292
|
- spec/kumogata_update_spec.rb
|
291
293
|
- spec/kumogata_validate_spec.rb
|
292
294
|
- spec/spec_helper.rb
|
295
|
+
- spec/string_stream_spec.rb
|