iostreams 0.14.0 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +202 -0
  3. data/README.md +155 -47
  4. data/lib/io_streams/file/reader.rb +7 -8
  5. data/lib/io_streams/file/writer.rb +7 -8
  6. data/lib/io_streams/io_streams.rb +313 -129
  7. data/lib/io_streams/{delimited → line}/reader.rb +20 -30
  8. data/lib/io_streams/line/writer.rb +81 -0
  9. data/lib/io_streams/pgp.rb +4 -14
  10. data/lib/io_streams/record/reader.rb +55 -0
  11. data/lib/io_streams/record/writer.rb +63 -0
  12. data/lib/io_streams/row/reader.rb +60 -0
  13. data/lib/io_streams/row/writer.rb +62 -0
  14. data/lib/io_streams/s3.rb +25 -0
  15. data/lib/io_streams/s3/reader.rb +64 -0
  16. data/lib/io_streams/s3/writer.rb +13 -0
  17. data/lib/io_streams/streams.rb +1 -1
  18. data/lib/io_streams/tabular.rb +163 -0
  19. data/lib/io_streams/tabular/errors.rb +14 -0
  20. data/lib/io_streams/tabular/header.rb +146 -0
  21. data/lib/io_streams/tabular/parser/array.rb +26 -0
  22. data/lib/io_streams/tabular/parser/base.rb +12 -0
  23. data/lib/io_streams/tabular/parser/csv.rb +35 -0
  24. data/lib/io_streams/tabular/parser/fixed.rb +88 -0
  25. data/lib/io_streams/tabular/parser/hash.rb +21 -0
  26. data/lib/io_streams/tabular/parser/json.rb +25 -0
  27. data/lib/io_streams/tabular/parser/psv.rb +34 -0
  28. data/lib/io_streams/tabular/utility/csv_row.rb +115 -0
  29. data/lib/io_streams/version.rb +2 -2
  30. data/lib/io_streams/xlsx/reader.rb +1 -1
  31. data/lib/io_streams/zip/reader.rb +1 -1
  32. data/lib/io_streams/zip/writer.rb +1 -1
  33. data/lib/iostreams.rb +21 -10
  34. data/test/bzip2_reader_test.rb +21 -22
  35. data/test/bzip2_writer_test.rb +38 -32
  36. data/test/file_reader_test.rb +19 -18
  37. data/test/file_writer_test.rb +23 -22
  38. data/test/files/test.json +3 -0
  39. data/test/gzip_reader_test.rb +21 -22
  40. data/test/gzip_writer_test.rb +35 -29
  41. data/test/io_streams_test.rb +137 -61
  42. data/test/line_reader_test.rb +105 -0
  43. data/test/line_writer_test.rb +50 -0
  44. data/test/pgp_reader_test.rb +29 -29
  45. data/test/pgp_test.rb +149 -195
  46. data/test/pgp_writer_test.rb +63 -62
  47. data/test/record_reader_test.rb +61 -0
  48. data/test/record_writer_test.rb +73 -0
  49. data/test/row_reader_test.rb +34 -0
  50. data/test/row_writer_test.rb +51 -0
  51. data/test/tabular_test.rb +184 -0
  52. data/test/xlsx_reader_test.rb +13 -17
  53. data/test/zip_reader_test.rb +21 -22
  54. data/test/zip_writer_test.rb +40 -36
  55. metadata +41 -17
  56. data/lib/io_streams/csv/reader.rb +0 -21
  57. data/lib/io_streams/csv/writer.rb +0 -20
  58. data/lib/io_streams/delimited/writer.rb +0 -67
  59. data/test/csv_reader_test.rb +0 -34
  60. data/test/csv_writer_test.rb +0 -35
  61. data/test/delimited_reader_test.rb +0 -115
  62. data/test/delimited_writer_test.rb +0 -44
@@ -1,92 +1,93 @@
1
1
  require_relative 'test_helper'
2
2
 
3
- module Streams
4
- class PgpWriterTest < Minitest::Test
5
- describe IOStreams::Pgp::Writer do
6
- before do
7
- file_name = File.join(File.dirname(__FILE__), 'files', 'text.txt')
8
- @data = File.read(file_name)
3
+ class PgpWriterTest < Minitest::Test
4
+ describe IOStreams::Pgp::Writer do
5
+ let :temp_file do
6
+ Tempfile.new('iostreams')
7
+ end
9
8
 
10
- @temp_file = Tempfile.new('iostreams')
11
- @file_name = @temp_file.to_path
12
- end
9
+ let :file_name do
10
+ temp_file.path
11
+ end
13
12
 
14
- after do
15
- @temp_file.delete if @temp_file
16
- end
13
+ let :decrypted do
14
+ file_name = File.join(File.dirname(__FILE__), 'files', 'text.txt')
15
+ File.read(file_name)
16
+ end
17
17
 
18
- describe '.open' do
19
- it 'writes encrypted text file' do
20
- IOStreams::Pgp::Writer.open(@file_name, recipient: 'receiver@example.org', binary: false) do |io|
21
- io.write(@data)
22
- end
18
+ after do
19
+ temp_file.delete
20
+ end
23
21
 
24
- result = IOStreams::Pgp::Reader.open(@file_name, passphrase: 'receiver_passphrase', binary: false) { |file| file.read }
25
- assert_equal @data, result
22
+ describe '.open' do
23
+ it 'writes encrypted text file' do
24
+ IOStreams::Pgp::Writer.open(file_name, recipient: 'receiver@example.org', binary: false) do |io|
25
+ io.write(decrypted)
26
26
  end
27
27
 
28
- it 'writes encrypted binary file' do
29
- binary_file_name = File.join(File.dirname(__FILE__), 'files', 'spreadsheet.xlsx')
30
- binary_data = File.open(binary_file_name, 'rb') { |file| file.read }
28
+ result = IOStreams::Pgp::Reader.open(file_name, passphrase: 'receiver_passphrase', binary: false) { |file| file.read }
29
+ assert_equal decrypted, result
30
+ end
31
31
 
32
- File.open(binary_file_name, 'rb') do |input|
33
- IOStreams::Pgp::Writer.open(@file_name, recipient: 'receiver@example.org') do |output|
34
- IOStreams.copy(input, output, 65535)
35
- end
36
- end
32
+ it 'writes encrypted binary file' do
33
+ binary_file_name = File.join(File.dirname(__FILE__), 'files', 'spreadsheet.xlsx')
34
+ binary_data = File.open(binary_file_name, 'rb') { |file| file.read }
37
35
 
38
- result = IOStreams::Pgp::Reader.open(@file_name, passphrase: 'receiver_passphrase') { |file| file.read }
39
- assert_equal binary_data, result
36
+ File.open(binary_file_name, 'rb') do |input|
37
+ IOStreams::Pgp::Writer.open(file_name, recipient: 'receiver@example.org') do |output|
38
+ IOStreams.copy(input, output)
39
+ end
40
40
  end
41
41
 
42
- it 'writes and signs encrypted file' do
43
- IOStreams::Pgp::Writer.open(@file_name, recipient: 'receiver@example.org', signer: 'sender@example.org', signer_passphrase: 'sender_passphrase') do |io|
44
- io.write(@data)
45
- end
42
+ result = IOStreams::Pgp::Reader.open(file_name, passphrase: 'receiver_passphrase') { |file| file.read }
43
+ assert_equal binary_data, result
44
+ end
46
45
 
47
- result = IOStreams::Pgp::Reader.open(@file_name, passphrase: 'receiver_passphrase') { |file| file.read }
48
- assert_equal @data, result
46
+ it 'writes and signs encrypted file' do
47
+ IOStreams::Pgp::Writer.open(file_name, recipient: 'receiver@example.org', signer: 'sender@example.org', signer_passphrase: 'sender_passphrase') do |io|
48
+ io.write(decrypted)
49
49
  end
50
50
 
51
- it 'fails with bad signer passphrase' do
52
- skip 'GnuPG v2.1 and above passes when it should not' if IOStreams::Pgp.pgp_version.to_f >= 2.1
53
- assert_raises IOStreams::Pgp::Failure do
54
- IOStreams::Pgp::Writer.open(@file_name, recipient: 'receiver@example.org', signer: 'sender@example.org', signer_passphrase: 'BAD') do |io|
55
- io.write(@data)
56
- end
51
+ result = IOStreams::Pgp::Reader.open(file_name, passphrase: 'receiver_passphrase') { |file| file.read }
52
+ assert_equal decrypted, result
53
+ end
54
+
55
+ it 'fails with bad signer passphrase' do
56
+ skip 'GnuPG v2.1 and above passes when it should not' if IOStreams::Pgp.pgp_version.to_f >= 2.1
57
+ assert_raises IOStreams::Pgp::Failure do
58
+ IOStreams::Pgp::Writer.open(file_name, recipient: 'receiver@example.org', signer: 'sender@example.org', signer_passphrase: 'BAD') do |io|
59
+ io.write(decrypted)
57
60
  end
58
61
  end
62
+ end
59
63
 
60
- it 'fails with bad recipient' do
61
- assert_raises IOStreams::Pgp::Failure do
62
- IOStreams::Pgp::Writer.open(@file_name, recipient: 'BAD@example.org', signer: 'sender@example.org', signer_passphrase: 'sender_passphrase') do |io|
63
- io.write(@data)
64
- # Allow process to terminate
65
- sleep 1
66
- io.write(@data)
67
- end
64
+ it 'fails with bad recipient' do
65
+ assert_raises IOStreams::Pgp::Failure do
66
+ IOStreams::Pgp::Writer.open(file_name, recipient: 'BAD@example.org', signer: 'sender@example.org', signer_passphrase: 'sender_passphrase') do |io|
67
+ io.write(decrypted)
68
+ # Allow process to terminate
69
+ sleep 1
70
+ io.write(decrypted)
68
71
  end
69
72
  end
73
+ end
70
74
 
71
- it 'fails with bad signer' do
72
- assert_raises IOStreams::Pgp::Failure do
73
- IOStreams::Pgp::Writer.open(@file_name, recipient: 'receiver@example.org', signer: 'BAD@example.org', signer_passphrase: 'sender_passphrase') do |io|
74
- io.write(@data)
75
- end
75
+ it 'fails with bad signer' do
76
+ assert_raises IOStreams::Pgp::Failure do
77
+ IOStreams::Pgp::Writer.open(file_name, recipient: 'receiver@example.org', signer: 'BAD@example.org', signer_passphrase: 'sender_passphrase') do |io|
78
+ io.write(decrypted)
76
79
  end
77
80
  end
81
+ end
78
82
 
79
- it 'fails with stream output' do
80
- string_io = StringIO.new
81
- assert_raises NotImplementedError do
82
- IOStreams::Pgp::Writer.open(string_io, recipient: 'receiver@example.org') do |io|
83
- io.write(@data)
84
- end
83
+ it 'fails with stream output' do
84
+ string_io = StringIO.new
85
+ assert_raises NotImplementedError do
86
+ IOStreams::Pgp::Writer.open(string_io, recipient: 'receiver@example.org') do |io|
87
+ io.write(decrypted)
85
88
  end
86
89
  end
87
-
88
90
  end
89
-
90
91
  end
91
92
  end
92
93
  end
@@ -0,0 +1,61 @@
1
+ require_relative 'test_helper'
2
+
3
+ class RecordReaderTest < Minitest::Test
4
+ describe IOStreams::Record::Reader do
5
+ let :file_name do
6
+ File.join(File.dirname(__FILE__), 'files', 'test.csv')
7
+ end
8
+
9
+ let :json_file_name do
10
+ File.join(File.dirname(__FILE__), 'files', 'test.json')
11
+ end
12
+
13
+ let :csv_rows do
14
+ CSV.read(file_name)
15
+ end
16
+
17
+ let :expected do
18
+ rows = csv_rows.dup
19
+ header = rows.shift
20
+ rows.collect { |row| header.zip(row).to_h }
21
+ end
22
+
23
+ describe '#each' do
24
+ it 'csv file' do
25
+ records = []
26
+ IOStreams::Record::Reader.open(file_name, cleanse_header: false) do |io|
27
+ io.each { |row| records << row }
28
+ end
29
+ assert_equal expected, records
30
+ end
31
+
32
+ it 'json file' do
33
+ records = []
34
+ IOStreams::Record::Reader.open(json_file_name, cleanse_header: false) do |input|
35
+ input.each { |row| records << row }
36
+ end
37
+ assert_equal expected, records
38
+ end
39
+
40
+ it 'stream' do
41
+ rows = []
42
+ IOStreams.line_reader(file_name) do |file|
43
+ IOStreams::Record::Reader.open(file, cleanse_header: false) do |io|
44
+ io.each { |row| rows << row }
45
+ end
46
+ end
47
+ assert_equal expected, rows
48
+ end
49
+ end
50
+
51
+ describe '#collect' do
52
+ it 'json file' do
53
+ records = IOStreams::Record::Reader.open(json_file_name) do |input|
54
+ input.collect { |record| record["state"] }
55
+ end
56
+ assert_equal expected.collect { |record| record["state"] }, records
57
+ end
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,73 @@
1
+ require_relative 'test_helper'
2
+ require 'csv'
3
+
4
+ class RecordWriterTest < Minitest::Test
5
+ describe IOStreams::Record::Writer do
6
+ let :csv_file_name do
7
+ File.join(File.dirname(__FILE__), 'files', 'test.csv')
8
+ end
9
+
10
+ let :json_file_name do
11
+ File.join(File.dirname(__FILE__), 'files', 'test.json')
12
+ end
13
+
14
+ let :raw_csv_data do
15
+ File.read(csv_file_name)
16
+ end
17
+
18
+ let :raw_json_data do
19
+ File.read(json_file_name)
20
+ end
21
+
22
+ let :csv_rows do
23
+ CSV.read(csv_file_name)
24
+ end
25
+
26
+ let :inputs do
27
+ rows = csv_rows.dup
28
+ header = rows.shift
29
+ rows.collect { |row| header.zip(row).to_h }
30
+ end
31
+
32
+ let :temp_file do
33
+ Tempfile.new('iostreams')
34
+ end
35
+
36
+ let :file_name do
37
+ temp_file.path
38
+ end
39
+
40
+ after do
41
+ temp_file.delete
42
+ end
43
+
44
+ describe '#<<' do
45
+ it 'file' do
46
+ IOStreams::Record::Writer.open(file_name) do |io|
47
+ inputs.each { |hash| io << hash }
48
+ end
49
+ result = File.read(file_name)
50
+ assert_equal raw_csv_data, result
51
+ end
52
+
53
+ it 'json file' do
54
+ IOStreams::Record::Writer.open(file_name, file_name: 'abc.json') do |io|
55
+ inputs.each { |hash| io << hash }
56
+ end
57
+ result = File.read(file_name)
58
+ assert_equal raw_json_data, result
59
+ end
60
+
61
+ it 'stream' do
62
+ io_string = StringIO.new
63
+ IOStreams::Line::Writer.open(io_string) do |io|
64
+ IOStreams::Record::Writer.open(io) do |stream|
65
+ inputs.each { |row| stream << row }
66
+ end
67
+ end
68
+ assert_equal raw_csv_data, io_string.string
69
+ end
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,34 @@
1
+ require_relative 'test_helper'
2
+
3
+ class RowReaderTest < Minitest::Test
4
+ describe IOStreams::Row::Reader do
5
+ let :file_name do
6
+ File.join(File.dirname(__FILE__), 'files', 'test.csv')
7
+ end
8
+
9
+ let :expected do
10
+ CSV.read(file_name)
11
+ end
12
+
13
+ describe '.open' do
14
+ it 'file' do
15
+ rows = []
16
+ IOStreams::Row::Reader.open(file_name) do |io|
17
+ io.each { |row| rows << row }
18
+ end
19
+ assert_equal expected, rows
20
+ end
21
+
22
+ it 'stream' do
23
+ rows = []
24
+ IOStreams.line_reader(file_name) do |file|
25
+ IOStreams::Row::Reader.open(file) do |io|
26
+ io.each { |row| rows << row }
27
+ end
28
+ end
29
+ assert_equal expected, rows
30
+ end
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,51 @@
1
+ require_relative 'test_helper'
2
+ require 'csv'
3
+
4
+ class RowWriterTest < Minitest::Test
5
+ describe IOStreams::Row::Writer do
6
+ let :csv_file_name do
7
+ File.join(File.dirname(__FILE__), 'files', 'test.csv')
8
+ end
9
+
10
+ let :raw_csv_data do
11
+ File.read(csv_file_name)
12
+ end
13
+
14
+ let :csv_rows do
15
+ CSV.read(csv_file_name)
16
+ end
17
+
18
+ let :temp_file do
19
+ Tempfile.new('iostreams')
20
+ end
21
+
22
+ let :file_name do
23
+ temp_file.path
24
+ end
25
+
26
+ after do
27
+ temp_file.delete
28
+ end
29
+
30
+ describe '.open' do
31
+ it 'file' do
32
+ IOStreams::Row::Writer.open(file_name) do |io|
33
+ csv_rows.each { |array| io << array }
34
+ end
35
+ result = File.read(file_name)
36
+ assert_equal raw_csv_data, result
37
+ end
38
+
39
+ it 'stream' do
40
+ io_string = StringIO.new
41
+ IOStreams::Line::Writer.open(io_string) do |io|
42
+ IOStreams::Row::Writer.open(io) do |stream|
43
+ csv_rows.each { |array| stream << array }
44
+ end
45
+ end
46
+ assert_equal raw_csv_data, io_string.string
47
+ end
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,184 @@
1
+ require_relative 'test_helper'
2
+
3
+ class TabularTest < Minitest::Test
4
+ describe IOStreams::Tabular do
5
+ let :format do
6
+ :csv
7
+ end
8
+
9
+ let :tabular do
10
+ IOStreams::Tabular.new(columns: ['first_field', 'second', 'third'], format: format)
11
+ end
12
+
13
+ describe '#parse_header' do
14
+ it 'parses and sets the csv header' do
15
+ tabular = IOStreams::Tabular.new(format: :csv)
16
+ header = tabular.parse_header('first field,Second,thirD')
17
+ assert_equal ['first field', 'Second', 'thirD'], header
18
+ assert_equal header, tabular.header.columns
19
+ end
20
+ end
21
+
22
+ describe '#cleanse_header!' do
23
+ describe 'cleanses' do
24
+ it 'a csv header' do
25
+ tabular = IOStreams::Tabular.new(columns: ['first field', 'Second', 'thirD'])
26
+ header = tabular.cleanse_header!
27
+ assert_equal ['first_field', "second", "third"], header
28
+ assert_equal header, tabular.header.columns
29
+ end
30
+
31
+ it 'white listed snake cased alphanumeric columns' do
32
+ tabular = IOStreams::Tabular.new(
33
+ columns: ['Ard Vark', 'password', 'robot version', '$$$'],
34
+ allowed_columns: %w( ard_vark robot_version )
35
+ )
36
+ expected_header = ['ard_vark', nil, 'robot_version', nil]
37
+ cleansed_header = tabular.cleanse_header!
38
+ assert_equal(expected_header, cleansed_header)
39
+ end
40
+ end
41
+
42
+ describe 'allowed_columns' do
43
+ before do
44
+ @allowed_columns = ['first', 'second', 'third', 'fourth', 'fifth']
45
+ end
46
+
47
+ it 'passes' do
48
+ tabular = IOStreams::Tabular.new(columns: [' first ', 'Second', 'thirD '], allowed_columns: @allowed_columns)
49
+ header = tabular.cleanse_header!
50
+ assert_equal ['first', 'second', 'third'], header
51
+ assert_equal header, tabular.header.columns
52
+ assert_equal @allowed_columns, tabular.header.allowed_columns
53
+ end
54
+
55
+ it 'nils columns not in the whitelist' do
56
+ tabular = IOStreams::Tabular.new(columns: [' first ', 'Unknown Column', 'thirD '], allowed_columns: @allowed_columns)
57
+ header = tabular.cleanse_header!
58
+ assert_equal ['first', nil, 'third'], header
59
+ end
60
+
61
+ it 'raises exception for columns not in the whitelist' do
62
+ tabular = IOStreams::Tabular.new(columns: [' first ', 'Unknown Column', 'thirD '], allowed_columns: @allowed_columns, skip_unknown: false)
63
+ exc = assert_raises IOStreams::Tabular::Errors::InvalidHeader do
64
+ tabular.cleanse_header!
65
+ end
66
+ assert_equal 'Unknown columns after cleansing: Unknown Column', exc.message
67
+ end
68
+
69
+ it 'raises exception missing required columns' do
70
+ required = ['first', 'second', 'fifth']
71
+ tabular = IOStreams::Tabular.new(columns: [' first ', 'Second', 'thirD '], allowed_columns: @allowed_columns, required_columns: required)
72
+ exc = assert_raises IOStreams::Tabular::Errors::InvalidHeader do
73
+ tabular.cleanse_header!
74
+ end
75
+ assert_equal 'Missing columns after cleansing: fifth', exc.message
76
+ end
77
+
78
+ it 'raises exception when no columns left' do
79
+ tabular = IOStreams::Tabular.new(columns: ['one', 'two', 'three'], allowed_columns: @allowed_columns)
80
+ exc = assert_raises IOStreams::Tabular::Errors::InvalidHeader do
81
+ tabular.cleanse_header!
82
+ end
83
+ assert_equal 'All columns are unknown after cleansing: one,two,three', exc.message
84
+ end
85
+ end
86
+ end
87
+
88
+ describe '#record_parse' do
89
+ describe ':array format' do
90
+ let :format do
91
+ :array
92
+ end
93
+
94
+ it 'renders' do
95
+ assert hash = tabular.record_parse([1, 2, 3])
96
+ assert_equal({'first_field' => 1, 'second' => 2, 'third' => 3}, hash)
97
+ end
98
+ end
99
+
100
+ it 'format :csv' do
101
+ assert hash = tabular.record_parse('1,2,3')
102
+ assert_equal({'first_field' => '1', 'second' => '2', 'third' => '3'}, hash)
103
+ end
104
+
105
+ describe ':hash format' do
106
+ let :format do
107
+ :hash
108
+ end
109
+
110
+ it 'renders' do
111
+ assert hash = tabular.record_parse('first_field' => 1, 'second' => 2, 'third' => 3)
112
+ assert_equal({'first_field' => 1, 'second' => 2, 'third' => 3}, hash)
113
+ end
114
+ end
115
+
116
+ describe ':json format' do
117
+ let :format do
118
+ :json
119
+ end
120
+
121
+ it 'renders' do
122
+ assert hash = tabular.record_parse('{"first_field":1,"second":2,"third":3}')
123
+ assert_equal({'first_field' => 1, 'second' => 2, 'third' => 3}, hash)
124
+ end
125
+ end
126
+
127
+ describe ':psv format' do
128
+ let :format do
129
+ :psv
130
+ end
131
+
132
+ it 'renders' do
133
+ assert hash = tabular.record_parse('1|2|3')
134
+ assert_equal({'first_field' => '1', 'second' => '2', 'third' => '3'}, hash)
135
+ end
136
+ end
137
+
138
+ it 'skips columns not in the whitelist' do
139
+ tabular.header.allowed_columns = ['first', 'second', 'third', 'fourth', 'fifth']
140
+ tabular.cleanse_header!
141
+ assert hash = tabular.record_parse('1,2,3')
142
+ assert_equal({'second' => '2', 'third' => '3'}, hash)
143
+ end
144
+
145
+ it 'handles missing values' do
146
+ assert hash = tabular.record_parse('1,2')
147
+ assert_equal({'first_field' => '1', 'second' => '2', 'third' => nil}, hash)
148
+ end
149
+ end
150
+
151
+ describe '#render' do
152
+ it 'renders an array of values' do
153
+ assert csv_string = tabular.render([5, 6, 9])
154
+ assert_equal '5,6,9', csv_string
155
+ end
156
+
157
+ it 'renders a hash' do
158
+ assert csv_string = tabular.render({'third' => '3', 'first_field' => '1'})
159
+ assert_equal '1,,3', csv_string
160
+ end
161
+
162
+ it 'renders a hash including nil and boolean' do
163
+ assert csv_string = tabular.render({'third' => true, 'first_field' => false, 'second' => nil})
164
+ assert_equal 'false,,true', csv_string
165
+ end
166
+
167
+ describe ':psv format' do
168
+ let :format do
169
+ :psv
170
+ end
171
+
172
+ it 'renders psv nil and boolean' do
173
+ assert psv_string = tabular.render({'third' => true, 'first_field' => false, 'second' => nil})
174
+ assert_equal 'false||true', psv_string
175
+ end
176
+
177
+ it 'renders psv numeric and pipe data' do
178
+ assert psv_string = tabular.render({'third' => 23, 'first_field' => 'a|b|c', 'second' => '|'})
179
+ assert_equal 'a:b:c|:|23', psv_string
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end