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.
@@ -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