filter_io 0.1.6 → 0.2.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/.gitignore +1 -1
- data/Gemfile +2 -6
- data/README.markdown +37 -31
- data/Rakefile +4 -52
- data/filter_io.gemspec +26 -0
- data/lib/filter_io/version.rb +3 -0
- data/lib/filter_io.rb +108 -104
- data/spec/filter_io_spec.rb +801 -0
- data/spec/spec_helper.rb +6 -0
- metadata +114 -56
- data/Gemfile.lock +0 -24
- data/test/filter_io_test.rb +0 -777
- data/test/test_helper.rb +0 -6
@@ -0,0 +1,801 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'stringio'
|
5
|
+
require 'tempfile'
|
6
|
+
require 'zlib'
|
7
|
+
|
8
|
+
describe FilterIO do
|
9
|
+
def matches_reference_io_behaviour(input)
|
10
|
+
expected_io = StringIO.new(input)
|
11
|
+
actual_io = FilterIO.new(StringIO.new(input))
|
12
|
+
|
13
|
+
results = [expected_io, actual_io].map do |io|
|
14
|
+
results = []
|
15
|
+
errors = []
|
16
|
+
positions = []
|
17
|
+
|
18
|
+
# call the block repeatedly until we get to EOF
|
19
|
+
# and once more at the end to check what happens at EOF
|
20
|
+
one_more_time = [true]
|
21
|
+
while !io.eof? || one_more_time.pop
|
22
|
+
begin
|
23
|
+
results << yield(io)
|
24
|
+
errors << nil
|
25
|
+
rescue Exception => e
|
26
|
+
results << nil
|
27
|
+
errors << [e.class, e.message]
|
28
|
+
end
|
29
|
+
positions << io.pos
|
30
|
+
raise 'Too many iterations' if results.size > 100
|
31
|
+
end
|
32
|
+
|
33
|
+
[results, errors, positions]
|
34
|
+
end
|
35
|
+
|
36
|
+
# compare the filtered output against the reference
|
37
|
+
results[0].zip(results[1]).each do |expected, actual|
|
38
|
+
expect(actual).to eq expected
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'works with an empty source' do
|
43
|
+
io = FilterIO.new(StringIO.new(''))
|
44
|
+
expect(io.bof?).to be_true
|
45
|
+
io = FilterIO.new(StringIO.new(''))
|
46
|
+
expect(io.eof?).to be_true
|
47
|
+
io = FilterIO.new(StringIO.new(''))
|
48
|
+
expect {
|
49
|
+
io.readchar
|
50
|
+
}.to raise_error EOFError
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'supports `eof?`' do
|
54
|
+
io = FilterIO.new(StringIO.new('x'))
|
55
|
+
expect(io.eof?).to be_false
|
56
|
+
expect(io.readchar.chr).to eq 'x'
|
57
|
+
expect(io.eof?).to be_true
|
58
|
+
expect(io.read).to eq ''
|
59
|
+
expect(io.read(8)).to eq nil
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'supports `bof?`' do
|
63
|
+
io = FilterIO.new(StringIO.new('x'))
|
64
|
+
expect(io.bof?).to be_true
|
65
|
+
expect(io.readchar.chr).to eq 'x'
|
66
|
+
expect(io.bof?).to be_false
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'can `readchar` with unicode characters' do
|
70
|
+
matches_reference_io_behaviour('Résume') { |io| io.readchar }
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'can `read` with unicode characters' do
|
74
|
+
(1..3).each do |read_size|
|
75
|
+
matches_reference_io_behaviour('Résume') { |io| io.read read_size }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'can `read` with unicode characters' do
|
80
|
+
matches_reference_io_behaviour('Résume') { |io| io.read }
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'can `gets` with unicode characters' do
|
84
|
+
matches_reference_io_behaviour("über\nrésumé") { |io| io.gets }
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'can filter unicode characters' do
|
88
|
+
input = 'Résumé Test'
|
89
|
+
expected = 'résumé test'
|
90
|
+
[2, nil].each do |block_size|
|
91
|
+
io = FilterIO.new(StringIO.new(input), :block_size => block_size) { |data| data.downcase }
|
92
|
+
actual = io.read
|
93
|
+
expect(actual).to eq expected
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'does not buffer forever with bad encoding' do
|
98
|
+
input = "123\xc3\xc34567890"
|
99
|
+
block_count = 0
|
100
|
+
io = FilterIO.new(StringIO.new(input), :block_size => 2) do |data|
|
101
|
+
block_count += 1
|
102
|
+
expect(data.size).to be <= 6
|
103
|
+
data
|
104
|
+
end
|
105
|
+
actual = io.read
|
106
|
+
input.force_encoding 'ASCII-8BIT'
|
107
|
+
actual.force_encoding 'ASCII-8BIT'
|
108
|
+
expect(actual).to eq input
|
109
|
+
expect(block_count).to be >= 3
|
110
|
+
end
|
111
|
+
|
112
|
+
def with_iso8859_1_test_file(internal_encoding)
|
113
|
+
Tempfile.open 'filter_io' do |tempfile|
|
114
|
+
File.open(tempfile.path, 'wb') do |io|
|
115
|
+
io.write "\xFCber\nR\xE9sum\xE9"
|
116
|
+
end
|
117
|
+
File.open(tempfile.path, :external_encoding => 'ISO-8859-1', :internal_encoding => internal_encoding) do |io|
|
118
|
+
yield io
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'converts ISO-8859-1 to UTF-8 using `gets`' do
|
124
|
+
with_iso8859_1_test_file 'UTF-8' do |io_raw|
|
125
|
+
expect(io_raw.readchar).to eq 'ü'
|
126
|
+
expect(io_raw.gets).to eq "ber\n"
|
127
|
+
str = io_raw.gets
|
128
|
+
expect(str.downcase).to eq 'résumé'
|
129
|
+
expect(str.encoding.name).to eq 'UTF-8'
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'converts ISO-8859-1 to raw using `gets`' do
|
134
|
+
with_iso8859_1_test_file nil do |io_raw|
|
135
|
+
expect(io_raw.readchar).to eq 'ü'.encode('ISO-8859-1')
|
136
|
+
expect(io_raw.gets).to eq "ber\n"
|
137
|
+
str = io_raw.gets
|
138
|
+
expect(str.downcase).to eq 'résumé'.encode('ISO-8859-1')
|
139
|
+
expect(str.encoding.name).to eq 'ISO-8859-1'
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'converts ISO-8859-1 to UTF-8 using `readchar`' do
|
144
|
+
with_iso8859_1_test_file 'UTF-8' do |io_raw|
|
145
|
+
io = FilterIO.new(io_raw)
|
146
|
+
"über\n".chars.each do |expected|
|
147
|
+
actual = io.readchar
|
148
|
+
expect(actual).to eq expected
|
149
|
+
expect(actual.encoding.name).to eq 'UTF-8'
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
it 'converts ISO-8859-1 to raw using `readchar`' do
|
155
|
+
with_iso8859_1_test_file nil do |io_raw|
|
156
|
+
io = FilterIO.new(io_raw)
|
157
|
+
"über\n".encode('ISO-8859-1').chars.each do |expected|
|
158
|
+
actual = io.readchar
|
159
|
+
expect(actual).to eq expected
|
160
|
+
expect(actual.encoding.name).to eq 'ISO-8859-1'
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'converts ISO-8859-1 to UTF-8 using `read`' do
|
166
|
+
with_iso8859_1_test_file 'UTF-8' do |io_raw|
|
167
|
+
io = FilterIO.new(io_raw)
|
168
|
+
expect(io.read(2)).to eq 'ü'.force_encoding('ASCII-8BIT')
|
169
|
+
expect(io.read(2).encoding.name).to eq 'ASCII-8BIT'
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
it 'converts ISO-8859-1 to raw using `read`' do
|
174
|
+
with_iso8859_1_test_file nil do |io_raw|
|
175
|
+
io = FilterIO.new(io_raw)
|
176
|
+
expect(io.read(1)).to eq 'ü'.encode('ISO-8859-1').force_encoding('ASCII-8BIT')
|
177
|
+
expect(io.read(2).encoding.name).to eq 'ASCII-8BIT'
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'converts ISO-8859-1 to UTF-8 using `lines`' do
|
182
|
+
with_iso8859_1_test_file 'UTF-8' do |io_raw|
|
183
|
+
io = FilterIO.new(io_raw)
|
184
|
+
expected = ["über\n", 'Résumé']
|
185
|
+
actual = io.lines.to_a
|
186
|
+
expect(actual).to eq expected
|
187
|
+
expect(actual[0].encoding.name).to eq 'UTF-8'
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
it 'converts ISO-8859-1 to raw using `lines`' do
|
192
|
+
with_iso8859_1_test_file nil do |io_raw|
|
193
|
+
io = FilterIO.new(io_raw)
|
194
|
+
expected = ["über\n", 'Résumé'].map { |str| str.encode('ISO-8859-1') }
|
195
|
+
actual = io.lines.to_a
|
196
|
+
expect(actual).to eq expected
|
197
|
+
expect(actual[0].encoding.name).to eq 'ISO-8859-1'
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'converts ISO-8859-1 to UTF-8 via a block' do
|
202
|
+
[1, 2, nil].each do |block_size|
|
203
|
+
expected = "über\nrésumé"
|
204
|
+
with_iso8859_1_test_file 'UTF-8' do |io_raw|
|
205
|
+
io = FilterIO.new(io_raw, :block_size => block_size) do |data, state|
|
206
|
+
if state.bof?
|
207
|
+
expect(data[0]).to eq 'ü'
|
208
|
+
end
|
209
|
+
expect(data.encoding.name).to eq 'UTF-8'
|
210
|
+
data.downcase
|
211
|
+
end
|
212
|
+
expect(io.readchar).to eq 'ü'
|
213
|
+
expect(io.gets.encoding.name).to eq 'UTF-8'
|
214
|
+
expect(io.read(4)).to eq 'rés'.force_encoding('ASCII-8BIT')
|
215
|
+
str = io.gets
|
216
|
+
expect(str).to eq 'umé'
|
217
|
+
expect(str.encoding.name).to eq 'UTF-8'
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
it 'converts ISO-8859-1 to raw via a block' do
|
223
|
+
[1, 2, nil].each do |block_size|
|
224
|
+
expected = "über\nrésumé".encode('ISO-8859-1')
|
225
|
+
with_iso8859_1_test_file 'ISO-8859-1' do |io_raw|
|
226
|
+
io = FilterIO.new(io_raw, :block_size => block_size) do |data, state|
|
227
|
+
if state.bof?
|
228
|
+
expect(data[0]).to eq 'ü'.encode('ISO-8859-1')
|
229
|
+
end
|
230
|
+
expect(data.encoding.name).to eq 'ISO-8859-1'
|
231
|
+
data.downcase
|
232
|
+
end
|
233
|
+
expect(io.readchar).to eq 'ü'.encode('ISO-8859-1')
|
234
|
+
expect(io.gets.encoding.name).to eq 'ISO-8859-1'
|
235
|
+
expect(io.read(3)).to eq 'rés'.encode('ISO-8859-1').force_encoding('ASCII-8BIT')
|
236
|
+
str = io.gets
|
237
|
+
expect(str).to eq 'umé'.encode('ISO-8859-1')
|
238
|
+
expect(str.encoding.name).to eq 'ISO-8859-1'
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
it 'supports a block returning mix of UTF-8 and ASCII-8BIT' do
|
244
|
+
input = "X\xE2\x80\x94Y\xe2\x80\x99"
|
245
|
+
input.force_encoding 'ASCII-8BIT'
|
246
|
+
io = FilterIO.new(StringIO.new(input), :block_size => 4) do |data, state|
|
247
|
+
data.force_encoding data[0] == 'Y' ? 'UTF-8' : 'ASCII-8BIT'
|
248
|
+
data
|
249
|
+
end
|
250
|
+
expect(io.read).to eq input
|
251
|
+
end
|
252
|
+
|
253
|
+
it 'supports `read`' do
|
254
|
+
input = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit'
|
255
|
+
io_reference = StringIO.new(input)
|
256
|
+
io = FilterIO.new(StringIO.new(input))
|
257
|
+
[10,5,4,8,7,nil,nil].each do |read_len|
|
258
|
+
expect(io.read(read_len)).to eq io_reference.read(read_len)
|
259
|
+
expect(io.pos).to eq io_reference.pos
|
260
|
+
if read_len
|
261
|
+
expect(io.readchar).to eq io_reference.readchar
|
262
|
+
else
|
263
|
+
expect {
|
264
|
+
io_reference.readchar
|
265
|
+
}.to raise_error EOFError
|
266
|
+
expect {
|
267
|
+
io.readchar
|
268
|
+
}.to raise_error EOFError
|
269
|
+
end
|
270
|
+
expect(io.pos).to eq io_reference.pos
|
271
|
+
expect(io.eof?).to eq io_reference.eof?
|
272
|
+
end
|
273
|
+
expect(io.read).to eq io_reference.read
|
274
|
+
expect(io.read(4)).to eq io_reference.read(4)
|
275
|
+
expect(io_reference.eof?).to be_true
|
276
|
+
expect(io.eof?).to be_true
|
277
|
+
end
|
278
|
+
|
279
|
+
it 'returns empty from read(0) before EOF' do
|
280
|
+
io = FilterIO.new(StringIO.new('foo'))
|
281
|
+
expect(io.read(0)).to eq ''
|
282
|
+
expect(io.pos).to eq 0
|
283
|
+
expect(io.eof?).to be_false
|
284
|
+
end
|
285
|
+
|
286
|
+
it 'returns empty from read(0) at EOF' do
|
287
|
+
io = FilterIO.new(StringIO.new(''))
|
288
|
+
expect(io.read(0)).to eq ''
|
289
|
+
expect(io.pos).to eq 0
|
290
|
+
expect(io.eof?).to be_true
|
291
|
+
end
|
292
|
+
|
293
|
+
it 'errors if attempting to read negative' do
|
294
|
+
io = FilterIO.new(StringIO.new('foo'))
|
295
|
+
expect(io.read(2)).to eq 'fo'
|
296
|
+
expect {
|
297
|
+
io.read(-1)
|
298
|
+
}.to raise_error ArgumentError
|
299
|
+
expect(io.pos).to eq 2
|
300
|
+
end
|
301
|
+
|
302
|
+
it 'allows filtering of input with a block' do
|
303
|
+
input = 'foo bar'
|
304
|
+
expected = 'FOO BAR'
|
305
|
+
io = FilterIO.new(StringIO.new(input)) do |data|
|
306
|
+
data.upcase
|
307
|
+
end
|
308
|
+
expect(io.read).to eq expected
|
309
|
+
end
|
310
|
+
|
311
|
+
it 'passes BOF and EOF state to the block' do
|
312
|
+
input = "Test String"
|
313
|
+
expected = ">>>*Test** Str**ing*<<<"
|
314
|
+
io = FilterIO.new(StringIO.new(input), :block_size => 4) do |data, state|
|
315
|
+
data = "*#{data}*"
|
316
|
+
data = ">>>#{data}" if state.bof?
|
317
|
+
data = "#{data}<<<" if state.eof?
|
318
|
+
data
|
319
|
+
end
|
320
|
+
expect(io.read).to eq expected
|
321
|
+
end
|
322
|
+
|
323
|
+
it 'passes a copy of the data to block (to prevent mutation bugs)' do
|
324
|
+
input = "foobar"
|
325
|
+
expected = [
|
326
|
+
['fo', true],
|
327
|
+
['foob', true],
|
328
|
+
['ar', false],
|
329
|
+
]
|
330
|
+
actual = []
|
331
|
+
io = FilterIO.new(StringIO.new(input), :block_size => 2) do |data, state|
|
332
|
+
actual << [data.dup, state.bof?]
|
333
|
+
data.upcase!
|
334
|
+
raise FilterIO::NeedMoreData if data == 'FO'
|
335
|
+
data
|
336
|
+
end
|
337
|
+
expect(io.read).to eq input.upcase
|
338
|
+
expect(actual).to eq expected
|
339
|
+
end
|
340
|
+
|
341
|
+
it 'can be used with Symbol#to_proc' do
|
342
|
+
input = 'foo bar'
|
343
|
+
expected = 'FOO BAR'
|
344
|
+
io = FilterIO.new StringIO.new(input), &:upcase
|
345
|
+
expect(io.read).to eq expected
|
346
|
+
end
|
347
|
+
|
348
|
+
it 'allows custom block size when used with read(nil)' do
|
349
|
+
[1,4,7,9,13,30].each do |block_size|
|
350
|
+
input = ('A'..'Z').to_a.join
|
351
|
+
expected = input.chars.enum_for(:each_slice, block_size).to_a.map(&:join).map { |x| "[#{x}]" }.join
|
352
|
+
io = FilterIO.new(StringIO.new(input), :block_size => block_size) do |data|
|
353
|
+
"[#{data}]"
|
354
|
+
end
|
355
|
+
expect(io.read).to eq expected
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
it 'allows custom block size when used with gets/readline' do
|
360
|
+
[1,4,7,9,13,30].each do |block_size|
|
361
|
+
input = "ABCDEFG\nHJIKLMNOP\n"
|
362
|
+
expected = input.chars.enum_for(:each_slice, block_size).to_a.map(&:join).map { |x| "[#{x}]" }.join.lines.to_a
|
363
|
+
io = FilterIO.new(StringIO.new(input), :block_size => block_size) do |data|
|
364
|
+
"[#{data}]"
|
365
|
+
end
|
366
|
+
actual = io.readlines
|
367
|
+
expect(actual).to eq expected
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
it 'allows block size to be different multiple from the input size' do
|
372
|
+
(1..5).each do |block_size|
|
373
|
+
input_str = ('A'..'Z').to_a.join
|
374
|
+
expected_str = input_str.chars.enum_for(:each_slice, block_size).map { |x| "[#{x.join}]" }.join
|
375
|
+
(1..5).each do |read_size|
|
376
|
+
expected = StringIO.new(expected_str)
|
377
|
+
actual = FilterIO.new(StringIO.new(input_str), :block_size => block_size) do |data|
|
378
|
+
"[#{data}]"
|
379
|
+
end
|
380
|
+
|
381
|
+
until expected.eof?
|
382
|
+
expect(actual.read(read_size)).to eq expected.read(read_size)
|
383
|
+
expect(actual.pos).to eq expected.pos
|
384
|
+
end
|
385
|
+
expect(actual.eof?).to eq expected.eof?
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
it 'allows the filtered I/O to be rewound' do
|
391
|
+
io = FilterIO.new(StringIO.new('foo bar baz'))
|
392
|
+
expect(io.read(5)).to eq 'foo b'
|
393
|
+
expect(io.read(4)).to eq 'ar b'
|
394
|
+
io.rewind
|
395
|
+
expect(io.read(3)).to eq 'foo'
|
396
|
+
expect(io.readchar.chr).to eq ' '
|
397
|
+
io.rewind
|
398
|
+
expect(io.readchar.chr).to eq 'f'
|
399
|
+
expect(io.read(2)).to eq 'oo'
|
400
|
+
end
|
401
|
+
|
402
|
+
it 're-reads from the source when rewound (resets buffer)' do
|
403
|
+
str = 'foobar'
|
404
|
+
io = FilterIO.new(StringIO.new(str))
|
405
|
+
expect(io.read(3)).to eq 'foo'
|
406
|
+
str.replace 'FooBar'
|
407
|
+
expect(io.read(3)).to eq 'Bar'
|
408
|
+
io.rewind
|
409
|
+
expect(io.read(3)).to eq 'Foo'
|
410
|
+
end
|
411
|
+
|
412
|
+
it 'can be rewound with block' do
|
413
|
+
input = 'abcdefghij'
|
414
|
+
expected = input[1..-1]
|
415
|
+
io = FilterIO.new(StringIO.new(input), :block_size => 4) do |data, state|
|
416
|
+
data = data[1..-1] if state.bof?
|
417
|
+
data
|
418
|
+
end
|
419
|
+
expect(io.read(2)).to eq 'bc'
|
420
|
+
expect(io.read(4)).to eq 'defg'
|
421
|
+
io.rewind
|
422
|
+
expect(io.read(2)).to eq 'bc'
|
423
|
+
expect(io.read(4)).to eq 'defg'
|
424
|
+
end
|
425
|
+
|
426
|
+
it 'supports `ungetc`' do
|
427
|
+
input = 'foobar'
|
428
|
+
io = FilterIO.new(StringIO.new(input))
|
429
|
+
expect(io.read(3)).to eq 'foo'
|
430
|
+
io.ungetc 'x'
|
431
|
+
io.ungetc 'y'[0].ord
|
432
|
+
expect(io.read(3)).to eq 'yxb'
|
433
|
+
(1..5).each do |i|
|
434
|
+
io.ungetc i.to_s
|
435
|
+
end
|
436
|
+
expect(io.read).to eq '54321ar'
|
437
|
+
expect(input).to eq 'foobar'
|
438
|
+
end
|
439
|
+
|
440
|
+
it 'allows block to request more data before processing a block' do
|
441
|
+
input = '1ab123456cde78f9ghij0'
|
442
|
+
expected = input.gsub(/\d+/, '[\0]')
|
443
|
+
(1..5).each do |block_size|
|
444
|
+
expected_size = 0
|
445
|
+
io = FilterIO.new(StringIO.new(input), :block_size => block_size) do |data, state|
|
446
|
+
expected_size += block_size
|
447
|
+
raise FilterIO::NeedMoreData if data =~ /\d\z/ && !state.eof?
|
448
|
+
unless state.eof?
|
449
|
+
expect(data.size).to eq expected_size
|
450
|
+
end
|
451
|
+
expected_size = 0
|
452
|
+
data.gsub(/\d+/, '[\0]')
|
453
|
+
end
|
454
|
+
expect(io.read).to eq expected
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
it 'passes a line ending normalisation example' do
|
459
|
+
input = "This\r\nis\r\ra\n\ntest\n\r\n\nstring\r\r\n.\n"
|
460
|
+
expected = "This\nis\n\na\n\ntest\n\n\nstring\n\n.\n"
|
461
|
+
(1..5).each do |block_size|
|
462
|
+
io = FilterIO.new(StringIO.new(input), :block_size => block_size) do |data, state|
|
463
|
+
raise FilterIO::NeedMoreData if data =~ /[\r\n]\z/ && !state.eof?
|
464
|
+
data.gsub(/\r\n|\r|\n/, "\n")
|
465
|
+
end
|
466
|
+
expect(io.read).to eq expected
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
it 'passes a character dropping example' do
|
471
|
+
input = "ab1cde23f1g4hijklmno567pqr8stu9vw0xyz"
|
472
|
+
expected = input.gsub(/\d+/, '')
|
473
|
+
(1..5).each do |block_size|
|
474
|
+
io = FilterIO.new(StringIO.new(input), :block_size => block_size) do |data|
|
475
|
+
data.gsub(/\d+/, '')
|
476
|
+
end
|
477
|
+
expect(io.pos).to eq 0
|
478
|
+
expect(io.read).to eq expected
|
479
|
+
expect(io.pos).to eq expected.size
|
480
|
+
end
|
481
|
+
end
|
482
|
+
|
483
|
+
it 'supports `getc`' do
|
484
|
+
matches_reference_io_behaviour('foo') { |io| io.getc }
|
485
|
+
end
|
486
|
+
|
487
|
+
it 'supports `gets` with no args' do
|
488
|
+
[
|
489
|
+
"",
|
490
|
+
"x",
|
491
|
+
"foo bar",
|
492
|
+
"foo\nbar",
|
493
|
+
"foo\nbar\nbaz\n"
|
494
|
+
].each do |input|
|
495
|
+
matches_reference_io_behaviour(input) { |io| io.gets }
|
496
|
+
end
|
497
|
+
end
|
498
|
+
|
499
|
+
it 'supports `gets` for entire content' do
|
500
|
+
[
|
501
|
+
"",
|
502
|
+
"x",
|
503
|
+
"foo bar",
|
504
|
+
"foo\nbar",
|
505
|
+
"foo\nbar\nbaz\n"
|
506
|
+
].each do |input|
|
507
|
+
matches_reference_io_behaviour(input) { |io| io.gets(nil) }
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
it 'supports `gets` with a separator' do
|
512
|
+
[
|
513
|
+
"",
|
514
|
+
"x",
|
515
|
+
"foo\nbar\rbaz\n",
|
516
|
+
"abc\rdef\rghi\r",
|
517
|
+
"abcxyz",
|
518
|
+
].each do |input|
|
519
|
+
["\r", "x"].each do |sep_string|
|
520
|
+
matches_reference_io_behaviour(input) { |io| io.gets(sep_string) }
|
521
|
+
end
|
522
|
+
end
|
523
|
+
end
|
524
|
+
|
525
|
+
it 'supports `gets` with a two character seperator' do
|
526
|
+
["o", "oo"].each do |sep_string|
|
527
|
+
matches_reference_io_behaviour("foobarhelloworld") { |io| io.gets(sep_string) }
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
it 'supports `gets` when retrieving whole paragraphs' do
|
532
|
+
{
|
533
|
+
"" => [],
|
534
|
+
"x" => ['x'],
|
535
|
+
"foo bar" => ["foo bar"],
|
536
|
+
"foo bar\n" => ["foo bar\n"],
|
537
|
+
"foo bar\n\n" => ["foo bar\n\n"],
|
538
|
+
"foo bar\n\n\n" => ["foo bar\n\n"],
|
539
|
+
"foo bar\nbaz" => ["foo bar\nbaz"],
|
540
|
+
"foo bar\n\nbaz" => ["foo bar\n\n", "baz"],
|
541
|
+
"foo bar\n\n\nbaz" => ["foo bar\n\n", "baz"],
|
542
|
+
"foo bar\n\nbaz\n" => ["foo bar\n\n", "baz\n"],
|
543
|
+
"foo bar\n\nbaz\n\n" => ["foo bar\n\n", "baz\n\n"],
|
544
|
+
"foo bar\n\nbaz\n\n\n" => ["foo bar\n\n", "baz\n\n"],
|
545
|
+
"\n\n\nfoo bar\n\nbaz\n\n\nabc\ndef" => ["foo bar\n\n", "baz\n\n", "abc\ndef"],
|
546
|
+
}.each do |input, expected|
|
547
|
+
io = FilterIO.new(StringIO.new(input))
|
548
|
+
actual = []
|
549
|
+
while para = io.gets('')
|
550
|
+
actual << para
|
551
|
+
end
|
552
|
+
expect(actual).to eq expected
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
556
|
+
it 'supports `readline`' do
|
557
|
+
[
|
558
|
+
"foo\nbar\n",
|
559
|
+
"foo\nbar\nbaz"
|
560
|
+
].each do |input|
|
561
|
+
matches_reference_io_behaviour(input) { |io| io.readline }
|
562
|
+
matches_reference_io_behaviour(input) { |io| io.readline("o") }
|
563
|
+
end
|
564
|
+
end
|
565
|
+
|
566
|
+
it 'supports `readlines`' do
|
567
|
+
[
|
568
|
+
"foo\nbar\n",
|
569
|
+
"foo\nbar\nbaz"
|
570
|
+
].each do |input|
|
571
|
+
matches_reference_io_behaviour(input) { |io| io.readlines }
|
572
|
+
matches_reference_io_behaviour(input) { |io| io.readlines("o") }
|
573
|
+
end
|
574
|
+
end
|
575
|
+
|
576
|
+
it 'supports reading lines with both `lines` and `gets`' do
|
577
|
+
io = FilterIO.new(StringIO.new("foo\nbar\nbaz"))
|
578
|
+
expected = [ ["foo\n", "bar\n"], ["baz", nil] ]
|
579
|
+
actual = []
|
580
|
+
retval = io.lines do |line|
|
581
|
+
actual << [line, io.gets]
|
582
|
+
end
|
583
|
+
expect(retval).to eq io
|
584
|
+
expect(actual).to eq expected
|
585
|
+
end
|
586
|
+
|
587
|
+
it 'supports using `lines` as an eumerator' do
|
588
|
+
io = FilterIO.new(StringIO.new("foo\nbar\nbaz"))
|
589
|
+
e = io.lines
|
590
|
+
expected = [ ["foo\n", "bar\n"], ["baz", nil] ]
|
591
|
+
actual = e.map { |line| [line, io.gets] }
|
592
|
+
expect(actual).to eq expected
|
593
|
+
end
|
594
|
+
|
595
|
+
it 'supports `seek` with absolute positions' do
|
596
|
+
io = FilterIO.new(StringIO.new("abcdef"))
|
597
|
+
|
598
|
+
# beginning
|
599
|
+
expect(io.readchar.chr).to eq 'a'
|
600
|
+
expect(io.pos).to eq 1
|
601
|
+
io.seek 0, IO::SEEK_SET
|
602
|
+
expect(io.readchar.chr).to eq 'a'
|
603
|
+
expect(io.pos).to eq 1
|
604
|
+
|
605
|
+
# same position
|
606
|
+
io.seek 1, IO::SEEK_SET
|
607
|
+
expect(io.readchar.chr).to eq 'b'
|
608
|
+
expect(io.pos).to eq 2
|
609
|
+
|
610
|
+
# backwards fail
|
611
|
+
expect {
|
612
|
+
io.seek 1, IO::SEEK_SET
|
613
|
+
}.to raise_error Errno::EINVAL
|
614
|
+
expect(io.readchar.chr).to eq 'c'
|
615
|
+
expect(io.pos).to eq 3
|
616
|
+
end
|
617
|
+
|
618
|
+
it 'supports `seek` with relative positions' do
|
619
|
+
io = FilterIO.new(StringIO.new("abcdef"))
|
620
|
+
|
621
|
+
# same pos
|
622
|
+
expect(io.read(2)).to eq 'ab'
|
623
|
+
expect(io.pos).to eq 2
|
624
|
+
io.seek 0, IO::SEEK_CUR
|
625
|
+
expect(io.pos).to eq 2
|
626
|
+
|
627
|
+
# backwards fail
|
628
|
+
expect(io.read(1)).to eq 'c'
|
629
|
+
expect(io.pos).to eq 3
|
630
|
+
expect {
|
631
|
+
io.seek(-1, IO::SEEK_CUR)
|
632
|
+
}.to raise_error Errno::EINVAL
|
633
|
+
expect(io.pos).to eq 3
|
634
|
+
|
635
|
+
# forwards fail
|
636
|
+
expect(io.pos).to eq 3
|
637
|
+
expect {
|
638
|
+
io.seek(2, IO::SEEK_CUR)
|
639
|
+
}.to raise_error Errno::EINVAL
|
640
|
+
expect(io.pos).to eq 3
|
641
|
+
|
642
|
+
# beginning
|
643
|
+
io.seek(-io.pos, IO::SEEK_CUR)
|
644
|
+
expect(io.pos).to eq 0
|
645
|
+
end
|
646
|
+
|
647
|
+
it 'does not support `seek` relative to EOF' do
|
648
|
+
io = FilterIO.new(StringIO.new("abcdef"))
|
649
|
+
expect {
|
650
|
+
io.seek(0, IO::SEEK_END)
|
651
|
+
}.to raise_error Errno::EINVAL
|
652
|
+
expect {
|
653
|
+
io.seek(6, IO::SEEK_END)
|
654
|
+
}.to raise_error Errno::EINVAL
|
655
|
+
expect {
|
656
|
+
io.seek(-6, IO::SEEK_END)
|
657
|
+
}.to raise_error Errno::EINVAL
|
658
|
+
end
|
659
|
+
|
660
|
+
it 'errors if `seek` is called with invalid whence' do
|
661
|
+
io = FilterIO.new(StringIO.new("abcdef"))
|
662
|
+
expect {
|
663
|
+
io.seek(0, 42)
|
664
|
+
}.to raise_error Errno::EINVAL
|
665
|
+
end
|
666
|
+
|
667
|
+
it 'raises EOF if block requests more data at EOF' do
|
668
|
+
input = "foo"
|
669
|
+
[2,3,6].each do |block_size|
|
670
|
+
[true, false].each do |always|
|
671
|
+
count = 0
|
672
|
+
io = FilterIO.new(StringIO.new(input), :block_size => block_size) do |data, state|
|
673
|
+
count += 1
|
674
|
+
raise FilterIO::NeedMoreData if state.eof? or always
|
675
|
+
data
|
676
|
+
end
|
677
|
+
expect {
|
678
|
+
io.readline
|
679
|
+
}.to raise_error EOFError
|
680
|
+
expected_count = block_size < input.size ? 2 : 1
|
681
|
+
expect(count).to eq expected_count
|
682
|
+
end
|
683
|
+
end
|
684
|
+
end
|
685
|
+
|
686
|
+
it 'supports returning unconsumed data from the block' do
|
687
|
+
# get consecutive unique characters from a feed
|
688
|
+
# this is similar to uniq(1) and STL's unique_copy
|
689
|
+
input = "122234435"
|
690
|
+
expected = "123435"
|
691
|
+
(1..5).each do |block_size|
|
692
|
+
io = FilterIO.new(StringIO.new(input), :block_size => block_size) do |data, state|
|
693
|
+
# grab all of the same character
|
694
|
+
data =~ /\A(.)\1*(?!\1)/ or raise 'No data'
|
695
|
+
# if there was nothing after it and we aren't at EOF...
|
696
|
+
# ...grab more data to make sure we're at the end
|
697
|
+
raise FilterIO::NeedMoreData if $'.empty? && !state.eof?
|
698
|
+
# return the matched character as data and re-buffer the rest
|
699
|
+
[$&[0], $']
|
700
|
+
end
|
701
|
+
expect(io.read).to eq expected
|
702
|
+
end
|
703
|
+
end
|
704
|
+
|
705
|
+
it 'supports requesting of more data by returning all data as unused' do
|
706
|
+
input = "foo\ntest\n\n12345\n678"
|
707
|
+
expected = input.gsub(/^.*$/) { |x| "#{$&.size} #{$&}" }
|
708
|
+
expected += "\n" unless expected =~ /\n\z/
|
709
|
+
|
710
|
+
block_count = 0
|
711
|
+
io = FilterIO.new StringIO.new(input), :block_size => 2 do |data, state|
|
712
|
+
block_count += 1
|
713
|
+
raise 'Too many retries' if block_count > 100
|
714
|
+
raise "Expected less data: #{data.inspect}" if data.size > 6
|
715
|
+
output = ''
|
716
|
+
while data =~ /(.*)\n/ || (state.eof? && data =~ /(.+)/)
|
717
|
+
output << "#{$1.size} #{$1}\n"
|
718
|
+
data = $'
|
719
|
+
end
|
720
|
+
[output, data]
|
721
|
+
end
|
722
|
+
actual = io.read
|
723
|
+
|
724
|
+
expect(actual).to eq expected
|
725
|
+
expect(block_count).to be >= 10
|
726
|
+
end
|
727
|
+
|
728
|
+
it 'supports `close`' do
|
729
|
+
[2, 16].each do |block_size|
|
730
|
+
source_io = StringIO.new("foo\nbar\nbaz")
|
731
|
+
filtered_io = FilterIO.new(source_io, :block_size => block_size, &:upcase)
|
732
|
+
|
733
|
+
expect(filtered_io.gets).to eq "FOO\n"
|
734
|
+
|
735
|
+
# close the filtered stream
|
736
|
+
filtered_io.close
|
737
|
+
|
738
|
+
# both the filtered and source stream should be closed
|
739
|
+
expect(source_io).to be_closed
|
740
|
+
expect(filtered_io).to be_closed
|
741
|
+
|
742
|
+
# futher reads should raise an error
|
743
|
+
expect {
|
744
|
+
filtered_io.gets
|
745
|
+
}.to raise_error IOError
|
746
|
+
|
747
|
+
# closing again should raise an error
|
748
|
+
expect {
|
749
|
+
filtered_io.close
|
750
|
+
}.to raise_error IOError
|
751
|
+
end
|
752
|
+
end
|
753
|
+
|
754
|
+
it 'raises an IO error if block returns nil' do
|
755
|
+
io = FilterIO.new(StringIO.new("foo")) { |data| nil }
|
756
|
+
expect {
|
757
|
+
io.read.to_a
|
758
|
+
}.to raise_error IOError
|
759
|
+
end
|
760
|
+
|
761
|
+
it 'can read from GzipReader stream in raw' do
|
762
|
+
input = "über résumé"
|
763
|
+
input.force_encoding 'ASCII-8BIT'
|
764
|
+
buffer = StringIO.new
|
765
|
+
out = Zlib::GzipWriter.new buffer
|
766
|
+
out.write input
|
767
|
+
out.finish
|
768
|
+
buffer.rewind
|
769
|
+
io = Zlib::GzipReader.new(buffer, :internal_encoding => 'ASCII-8BIT')
|
770
|
+
|
771
|
+
io = FilterIO.new(io)
|
772
|
+
expect(io.readchar).to eq input[0]
|
773
|
+
expect(io.readchar).to eq input[1]
|
774
|
+
expect(io.read).to eq "ber résumé".force_encoding('ASCII-8BIT')
|
775
|
+
end
|
776
|
+
|
777
|
+
it 'can read from GzipReader stream in UTF-8' do
|
778
|
+
input = "über résumé"
|
779
|
+
buffer = StringIO.new
|
780
|
+
out = Zlib::GzipWriter.new buffer
|
781
|
+
out.write input
|
782
|
+
out.finish
|
783
|
+
buffer.rewind
|
784
|
+
io = Zlib::GzipReader.new(buffer)
|
785
|
+
|
786
|
+
io = FilterIO.new(io)
|
787
|
+
expect(io.readchar).to eq "ü"
|
788
|
+
expect(io.readchar).to eq "b"
|
789
|
+
expect(io.read).to eq "er résumé"
|
790
|
+
end
|
791
|
+
|
792
|
+
it 'supports filtering from a pipe' do
|
793
|
+
read_io, write_io = IO::pipe
|
794
|
+
write_io.write 'test'
|
795
|
+
write_io.close
|
796
|
+
io = FilterIO.new read_io do |data|
|
797
|
+
data.upcase
|
798
|
+
end
|
799
|
+
expect(io.read).to eq 'TEST'
|
800
|
+
end
|
801
|
+
end
|