adsp 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/AUTHORS +1 -0
  3. data/LICENSE +21 -0
  4. data/README.md +300 -0
  5. data/lib/adsp/error.rb +14 -0
  6. data/lib/adsp/file.rb +76 -0
  7. data/lib/adsp/option.rb +51 -0
  8. data/lib/adsp/stream/abstract.rb +206 -0
  9. data/lib/adsp/stream/delegates.rb +39 -0
  10. data/lib/adsp/stream/raw/abstract.rb +69 -0
  11. data/lib/adsp/stream/raw/compressor.rb +110 -0
  12. data/lib/adsp/stream/raw/decompressor.rb +80 -0
  13. data/lib/adsp/stream/raw/native_compressor.rb +58 -0
  14. data/lib/adsp/stream/raw/native_decompressor.rb +44 -0
  15. data/lib/adsp/stream/reader.rb +234 -0
  16. data/lib/adsp/stream/reader_helpers.rb +219 -0
  17. data/lib/adsp/stream/stat.rb +80 -0
  18. data/lib/adsp/stream/writer.rb +206 -0
  19. data/lib/adsp/stream/writer_helpers.rb +102 -0
  20. data/lib/adsp/string.rb +58 -0
  21. data/lib/adsp/validation.rb +46 -0
  22. data/lib/adsp/version.rb +7 -0
  23. data/lib/adsp.rb +8 -0
  24. data/test/common.rb +108 -0
  25. data/test/coverage_helper.rb +18 -0
  26. data/test/file.test.rb +120 -0
  27. data/test/minitest.rb +20 -0
  28. data/test/mock/common.rb +57 -0
  29. data/test/mock/file.rb +60 -0
  30. data/test/mock/stream/raw/compressor.rb +20 -0
  31. data/test/mock/stream/raw/decompressor.rb +20 -0
  32. data/test/mock/stream/raw/native_compressor.rb +82 -0
  33. data/test/mock/stream/raw/native_decompressor.rb +70 -0
  34. data/test/mock/stream/reader.rb +18 -0
  35. data/test/mock/stream/writer.rb +18 -0
  36. data/test/mock/string.rb +44 -0
  37. data/test/option.rb +66 -0
  38. data/test/stream/abstract.rb +125 -0
  39. data/test/stream/minitar.test.rb +50 -0
  40. data/test/stream/raw/abstract.rb +45 -0
  41. data/test/stream/raw/compressor.test.rb +166 -0
  42. data/test/stream/raw/decompressor.test.rb +166 -0
  43. data/test/stream/reader.test.rb +643 -0
  44. data/test/stream/reader_helpers.test.rb +421 -0
  45. data/test/stream/writer.test.rb +610 -0
  46. data/test/stream/writer_helpers.test.rb +267 -0
  47. data/test/string.test.rb +95 -0
  48. data/test/validation.rb +71 -0
  49. data/test/version.test.rb +18 -0
  50. metadata +274 -0
@@ -0,0 +1,610 @@
1
+ # Abstract data stream processor.
2
+ # Copyright (c) 2021 AUTHORS, MIT License.
3
+
4
+ require "set"
5
+ require "socket"
6
+ require "stringio"
7
+
8
+ require_relative "abstract"
9
+ require_relative "../common"
10
+ require_relative "../minitest"
11
+ require_relative "../option"
12
+ require_relative "../mock/stream/writer"
13
+ require_relative "../mock/string"
14
+
15
+ module ADSP
16
+ module Test
17
+ module Stream
18
+ class Writer < Abstract
19
+ Target = Mock::Stream::Writer
20
+ String = Mock::String
21
+
22
+ ARCHIVE_PATH = Common::ARCHIVE_PATH
23
+ ENCODINGS = Common::ENCODINGS
24
+ TRANSCODE_OPTIONS = Common::TRANSCODE_OPTIONS
25
+ TEXTS = Common::TEXTS
26
+ LARGE_TEXTS = Common::LARGE_TEXTS
27
+ PORTION_LENGTHS = Common::PORTION_LENGTHS
28
+ LARGE_PORTION_LENGTHS = Common::LARGE_PORTION_LENGTHS
29
+
30
+ BUFFER_LENGTH_NAMES = %i[destination_buffer_length].freeze
31
+ BUFFER_LENGTH_MAPPING = { :destination_buffer_length => :destination_buffer_length }.freeze
32
+ FINISH_MODES = OCG.new(
33
+ :flush_nonblock => Option::BOOLS,
34
+ :close_nonblock => Option::BOOLS
35
+ )
36
+ .freeze
37
+
38
+ NONBLOCK_SERVER_MODES = {
39
+ :request => 0,
40
+ :response => 1
41
+ }
42
+ .freeze
43
+
44
+ NONBLOCK_SERVER_TIMEOUT = 0.1
45
+
46
+ def initialize(*args)
47
+ super(*args)
48
+
49
+ @nonblock_client_lock = ::Mutex.new
50
+ @nonblock_client_id = 0
51
+ end
52
+
53
+ def test_invalid_initialize
54
+ get_invalid_compressor_options do |invalid_options|
55
+ assert_raises ValidateError do
56
+ target.new ::StringIO.new, invalid_options
57
+ end
58
+ end
59
+
60
+ super
61
+ end
62
+
63
+ # -- synchronous --
64
+
65
+ def test_invalid_write
66
+ instance = target.new Validation::StringIOWithoutWrite.new
67
+
68
+ assert_raises ValidateError do
69
+ instance.write ""
70
+ end
71
+
72
+ assert_raises ValidateError do
73
+ instance.flush
74
+ end
75
+
76
+ assert_raises ValidateError do
77
+ instance.rewind
78
+ end
79
+
80
+ assert_raises ValidateError do
81
+ instance.close
82
+ end
83
+ end
84
+
85
+ def test_write
86
+ parallel_compressor_options do |compressor_options|
87
+ TEXTS.each do |text|
88
+ PORTION_LENGTHS.each do |portion_length|
89
+ sources = get_sources text, portion_length
90
+ io = ::StringIO.new
91
+ instance = target.new io, compressor_options
92
+
93
+ begin
94
+ sources.each_slice 2 do |current_sources|
95
+ instance.write(*current_sources)
96
+ instance.flush
97
+ end
98
+
99
+ assert_equal instance.pos, text.bytesize
100
+ assert_equal instance.pos, instance.tell
101
+ ensure
102
+ refute_predicate instance, :closed?
103
+ instance.close
104
+ assert_predicate instance, :closed?
105
+ end
106
+
107
+ compressed_text = io.string
108
+
109
+ get_compatible_decompressor_options compressor_options do |decompressor_options|
110
+ check_text text, compressed_text, decompressor_options
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ def test_write_with_large_texts
118
+ options_generator = OCG.new(
119
+ :text => LARGE_TEXTS,
120
+ :portion_length => LARGE_PORTION_LENGTHS
121
+ )
122
+
123
+ Common.parallel_options options_generator do |options|
124
+ text = options[:text]
125
+ portion_length = options[:portion_length]
126
+
127
+ sources = get_sources text, portion_length
128
+ io = ::StringIO.new
129
+ instance = target.new io
130
+
131
+ begin
132
+ sources.each_slice 2 do |current_sources|
133
+ instance.write(*current_sources)
134
+ instance.flush
135
+ end
136
+ ensure
137
+ instance.close
138
+ end
139
+
140
+ compressed_text = io.string
141
+ check_text text, compressed_text
142
+ end
143
+ end
144
+
145
+ def test_encoding
146
+ parallel_compressor_options do |compressor_options|
147
+ TEXTS.each do |text|
148
+ # We don't need to transcode between same encodings.
149
+ (ENCODINGS - [text.encoding]).each do |external_encoding|
150
+ target_text = text.encode external_encoding, **TRANSCODE_OPTIONS
151
+ io = ::StringIO.new
152
+
153
+ instance = target.new(
154
+ io,
155
+ compressor_options,
156
+ :external_encoding => external_encoding,
157
+ :transcode_options => TRANSCODE_OPTIONS
158
+ )
159
+
160
+ assert_equal external_encoding, instance.external_encoding
161
+ assert_equal TRANSCODE_OPTIONS, instance.transcode_options
162
+
163
+ begin
164
+ instance.set_encoding external_encoding, nil, TRANSCODE_OPTIONS
165
+ assert_equal external_encoding, instance.external_encoding
166
+ assert_equal TRANSCODE_OPTIONS, instance.transcode_options
167
+
168
+ instance.write text
169
+ ensure
170
+ instance.close
171
+ end
172
+
173
+ compressed_text = io.string
174
+
175
+ get_compatible_decompressor_options compressor_options do |decompressor_options|
176
+ check_text target_text, compressed_text, decompressor_options
177
+ assert_predicate target_text, :valid_encoding?
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ def test_rewind
185
+ parallel_compressor_options do |compressor_options, worker_index|
186
+ archive_path = Common.get_path ARCHIVE_PATH, worker_index
187
+
188
+ compressed_texts = []
189
+
190
+ ::File.open archive_path, "wb" do |file|
191
+ instance = target.new file, compressor_options
192
+
193
+ begin
194
+ TEXTS.each do |text|
195
+ instance.write text
196
+ instance.flush
197
+
198
+ assert_equal instance.pos, text.bytesize
199
+ assert_equal instance.pos, instance.tell
200
+
201
+ assert_equal 0, instance.rewind
202
+
203
+ compressed_texts << ::File.read(archive_path, :mode => "rb")
204
+
205
+ assert_equal 0, instance.pos
206
+ assert_equal instance.pos, instance.tell
207
+
208
+ file.truncate 0
209
+ end
210
+ ensure
211
+ instance.close
212
+ end
213
+ end
214
+
215
+ TEXTS.each.with_index do |text, index|
216
+ compressed_text = compressed_texts[index]
217
+
218
+ get_compatible_decompressor_options compressor_options do |decompressor_options|
219
+ check_text text, compressed_text, decompressor_options
220
+ end
221
+ end
222
+ end
223
+ end
224
+
225
+ # -- asynchronous --
226
+
227
+ def test_invalid_write_nonblock
228
+ instance = target.new Validation::StringIOWithoutWriteNonblock.new
229
+
230
+ assert_raises ValidateError do
231
+ instance.write_nonblock ""
232
+ end
233
+
234
+ assert_raises ValidateError do
235
+ instance.flush_nonblock
236
+ end
237
+
238
+ assert_raises ValidateError do
239
+ instance.close_nonblock
240
+ end
241
+ end
242
+
243
+ def test_write_nonblock
244
+ nonblock_server do |server|
245
+ parallel_compressor_options do |compressor_options|
246
+ TEXTS.each do |text|
247
+ PORTION_LENGTHS.each do |portion_length|
248
+ sources = get_sources text, portion_length
249
+
250
+ FINISH_MODES.each do |finish_mode|
251
+ nonblock_test server, text, portion_length, compressor_options do |instance, socket|
252
+ # write
253
+
254
+ sources.each.with_index do |source, index|
255
+ if index.even?
256
+ loop do
257
+ begin
258
+ bytes_written = instance.write_nonblock source
259
+ rescue ::IO::WaitWritable
260
+ socket.wait_writable
261
+ retry
262
+ end
263
+
264
+ source = source.byteslice bytes_written, source.bytesize - bytes_written
265
+ break if source.bytesize.zero?
266
+ end
267
+ else
268
+ instance.write source
269
+ end
270
+ end
271
+
272
+ # flush
273
+
274
+ if finish_mode[:flush_nonblock]
275
+ loop do
276
+ begin
277
+ is_flushed = instance.flush_nonblock
278
+ rescue ::IO::WaitWritable
279
+ socket.wait_writable
280
+ retry
281
+ end
282
+
283
+ break if is_flushed
284
+ end
285
+ else
286
+ instance.flush
287
+ end
288
+
289
+ assert_equal instance.pos, text.bytesize
290
+ assert_equal instance.pos, instance.tell
291
+
292
+ ensure
293
+ # close
294
+
295
+ refute_predicate instance, :closed?
296
+
297
+ if finish_mode[:close_nonblock]
298
+ loop do
299
+ begin
300
+ is_closed = instance.close_nonblock
301
+ rescue ::IO::WaitWritable
302
+ socket.wait_writable
303
+ retry
304
+ end
305
+
306
+ break if is_closed
307
+ end
308
+ else
309
+ instance.close
310
+ end
311
+
312
+ assert_predicate instance, :closed?
313
+ end
314
+ end
315
+ end
316
+ end
317
+ end
318
+ end
319
+ end
320
+
321
+ def test_write_nonblock_with_large_texts
322
+ nonblock_server do |server|
323
+ Common.parallel LARGE_TEXTS do |text|
324
+ LARGE_PORTION_LENGTHS.each do |portion_length|
325
+ sources = get_sources text, portion_length
326
+
327
+ FINISH_MODES.each do |finish_mode|
328
+ nonblock_test server, text, portion_length do |instance, socket|
329
+ # write
330
+
331
+ sources.each.with_index do |source, index|
332
+ if index.even?
333
+ loop do
334
+ begin
335
+ bytes_written = instance.write_nonblock source
336
+ rescue ::IO::WaitWritable
337
+ socket.wait_writable
338
+ retry
339
+ end
340
+
341
+ source = source.byteslice bytes_written, source.bytesize - bytes_written
342
+ break if source.bytesize.zero?
343
+ end
344
+ else
345
+ instance.write source
346
+ end
347
+ end
348
+
349
+ # flush
350
+
351
+ if finish_mode[:flush_nonblock]
352
+ loop do
353
+ begin
354
+ is_flushed = instance.flush_nonblock
355
+ rescue ::IO::WaitWritable
356
+ socket.wait_writable
357
+ retry
358
+ end
359
+
360
+ break if is_flushed
361
+ end
362
+ else
363
+ instance.flush
364
+ end
365
+
366
+ ensure
367
+ # close
368
+
369
+ if finish_mode[:close_nonblock]
370
+ loop do
371
+ begin
372
+ is_closed = instance.close_nonblock
373
+ rescue ::IO::WaitWritable
374
+ socket.wait_writable
375
+ retry
376
+ end
377
+
378
+ break if is_closed
379
+ end
380
+ else
381
+ instance.close
382
+ end
383
+ end
384
+ end
385
+ end
386
+ end
387
+ end
388
+ end
389
+
390
+ def test_invalid_rewind_nonblock
391
+ instance = target.new Validation::StringIOWithoutWriteNonblock.new
392
+
393
+ assert_raises ValidateError do
394
+ instance.rewind_nonblock
395
+ end
396
+ end
397
+
398
+ def test_rewind_nonblock
399
+ return unless Common.file_can_be_used_nonblock?
400
+
401
+ parallel_compressor_options do |compressor_options, worker_index|
402
+ archive_path = Common.get_path ARCHIVE_PATH, worker_index
403
+
404
+ compressed_texts = []
405
+
406
+ ::File.open archive_path, "wb" do |file|
407
+ instance = target.new file, compressor_options
408
+
409
+ begin
410
+ TEXTS.each do |text|
411
+ instance.write text
412
+ instance.flush
413
+
414
+ assert_equal instance.pos, text.bytesize
415
+ assert_equal instance.pos, instance.tell
416
+
417
+ loop do
418
+ begin
419
+ is_rewinded = instance.rewind_nonblock
420
+ rescue ::IO::WaitWritable
421
+ file.wait_writable
422
+ retry
423
+ end
424
+
425
+ break if is_rewinded
426
+ end
427
+
428
+ compressed_texts << ::File.read(archive_path, :mode => "rb")
429
+
430
+ assert_equal 0, instance.pos
431
+ assert_equal instance.pos, instance.tell
432
+
433
+ file.truncate 0
434
+ end
435
+ ensure
436
+ instance.close
437
+ end
438
+ end
439
+
440
+ TEXTS.each.with_index do |text, index|
441
+ compressed_text = compressed_texts[index]
442
+
443
+ get_compatible_decompressor_options compressor_options do |decompressor_options|
444
+ check_text text, compressed_text, decompressor_options
445
+ end
446
+ end
447
+ end
448
+ end
449
+
450
+ # -- nonblock test --
451
+
452
+ protected def nonblock_server
453
+ # We need to test close nonblock.
454
+ # This method writes remaining data and closes socket.
455
+ # Server is not able to send response immediately.
456
+ # Client has to reconnect to server once again.
457
+
458
+ ::TCPServer.open 0 do |server|
459
+ # Server loop will be processed in separate (parent) thread.
460
+ # Child threads will be collected for later usage.
461
+ child_lock = ::Mutex.new
462
+ child_threads = ::Set.new
463
+
464
+ # Server need to maintain mapping between client id and result.
465
+ results_lock = ::Mutex.new
466
+ results = {}
467
+
468
+ parent_thread = ::Thread.new do
469
+ loop do
470
+ child_thread = ::Thread.start server.accept do |socket|
471
+ # Reading head.
472
+ client_id, portion_length, mode = socket.read(13).unpack "NQC"
473
+
474
+ if mode == NONBLOCK_SERVER_MODES[:request]
475
+ # Reading result from client.
476
+ result = "".b
477
+
478
+ loop do
479
+ result << socket.read_nonblock(portion_length)
480
+ rescue ::IO::WaitReadable
481
+ socket.wait_readable
482
+ rescue ::EOFError
483
+ break
484
+ end
485
+
486
+ # Saving result for client.
487
+ results_lock.synchronize { results[client_id] = result }
488
+
489
+ next
490
+ end
491
+
492
+ loop do
493
+ # Waiting when result will be ready.
494
+ result = results_lock.synchronize { results[client_id] }
495
+
496
+ unless result.nil?
497
+ # Sending result to client.
498
+ socket.write result
499
+
500
+ break
501
+ end
502
+
503
+ sleep NONBLOCK_SERVER_TIMEOUT
504
+ end
505
+
506
+ # Removing result for client.
507
+ results_lock.synchronize { results.delete client_id }
508
+
509
+ ensure
510
+ socket.close
511
+
512
+ # Removing current child thread.
513
+ child_lock.synchronize { child_threads.delete ::Thread.current }
514
+ end
515
+
516
+ # Adding new child thread.
517
+ child_lock.synchronize { child_threads.add child_thread }
518
+ end
519
+ end
520
+
521
+ # Processing client.
522
+ begin
523
+ yield server
524
+ ensure
525
+ # We need to kill parent thread when client has finished.
526
+ # So server won't be able to create new child threads.
527
+ # Than we can join all remaining child threads.
528
+ parent_thread.kill.join
529
+ child_threads.each(&:join)
530
+ end
531
+ end
532
+ end
533
+
534
+ protected def nonblock_test(server, text, portion_length, compressor_options = {}, &_block)
535
+ port = server.addr[1]
536
+ client_id = @nonblock_client_lock.synchronize { @nonblock_client_id += 1 }
537
+
538
+ # Writing request.
539
+ ::TCPSocket.open "localhost", port do |socket|
540
+ # Writing head.
541
+ head = [client_id, portion_length, NONBLOCK_SERVER_MODES[:request]].pack "NQC"
542
+ socket.write head
543
+
544
+ # Instance is going to write compressed text.
545
+ instance = target.new socket, compressor_options
546
+
547
+ begin
548
+ yield instance, socket
549
+ ensure
550
+ instance.close
551
+ end
552
+ end
553
+
554
+ # Reading response.
555
+ compressed_text = ::TCPSocket.open "localhost", port do |socket|
556
+ # Writing head.
557
+ head = [client_id, portion_length, NONBLOCK_SERVER_MODES[:response]].pack "NQC"
558
+ socket.write head
559
+
560
+ # Reading compressed text.
561
+ socket.read
562
+ end
563
+
564
+ # Testing compressed text.
565
+ if compressor_options.empty?
566
+ check_text text, compressed_text
567
+ else
568
+ get_compatible_decompressor_options compressor_options do |decompressor_options|
569
+ check_text text, compressed_text, decompressor_options
570
+ end
571
+ end
572
+ end
573
+
574
+ # -----
575
+
576
+ protected def get_sources(text, portion_length)
577
+ sources = text
578
+ .chars
579
+ .each_slice(portion_length)
580
+ .map(&:join)
581
+
582
+ return ["".b] if sources.empty?
583
+
584
+ sources
585
+ end
586
+
587
+ protected def check_text(text, compressed_text, decompressor_options = {})
588
+ decompressed_text = String.decompress compressed_text, decompressor_options
589
+ decompressed_text.force_encoding text.encoding
590
+
591
+ assert_equal text, decompressed_text
592
+ end
593
+
594
+ def get_invalid_compressor_options(&block)
595
+ Option.get_invalid_compressor_options BUFFER_LENGTH_NAMES, &block
596
+ end
597
+
598
+ def parallel_compressor_options(&block)
599
+ Common.parallel_options Option.get_compressor_options_generator(BUFFER_LENGTH_NAMES), &block
600
+ end
601
+
602
+ def get_compatible_decompressor_options(compressor_options, &block)
603
+ Option.get_compatible_decompressor_options compressor_options, BUFFER_LENGTH_MAPPING, &block
604
+ end
605
+ end
606
+
607
+ Minitest << Writer
608
+ end
609
+ end
610
+ end