filter_io 0.1.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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