diff options
Diffstat (limited to 'test/csv')
33 files changed, 3241 insertions, 919 deletions
diff --git a/test/csv/base.rb b/test/csv/base.rb deleted file mode 100644 index 621569e013..0000000000 --- a/test/csv/base.rb +++ /dev/null @@ -1,8 +0,0 @@ -require "test/unit" - -require "csv" - -require_relative "../lib/with_different_ofs.rb" - -class TestCSV < Test::Unit::TestCase -end diff --git a/test/csv/helper.rb b/test/csv/helper.rb new file mode 100644 index 0000000000..1f9cf96979 --- /dev/null +++ b/test/csv/helper.rb @@ -0,0 +1,42 @@ +require "tempfile" +require "test/unit" + +require "csv" + +require_relative "../lib/with_different_ofs" + +module Helper + def with_chunk_size(chunk_size) + chunk_size_keep = ENV["CSV_PARSER_SCANNER_TEST_CHUNK_SIZE"] + begin + ENV["CSV_PARSER_SCANNER_TEST_CHUNK_SIZE"] = chunk_size + yield + ensure + ENV["CSV_PARSER_SCANNER_TEST_CHUNK_SIZE"] = chunk_size_keep + end + end + + def with_verbose(verbose) + original = $VERBOSE + begin + $VERBOSE = verbose + yield + ensure + $VERBOSE = original + end + end + + def with_default_internal(encoding) + original = Encoding.default_internal + begin + with_verbose(false) do + Encoding.default_internal = encoding + end + yield + ensure + with_verbose(false) do + Encoding.default_internal = original + end + end + end +end diff --git a/test/csv/interface/test_delegation.rb b/test/csv/interface/test_delegation.rb new file mode 100644 index 0000000000..349257633b --- /dev/null +++ b/test/csv/interface/test_delegation.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: false + +require_relative "../helper" + +class TestCSVInterfaceDelegation < Test::Unit::TestCase + class TestStringIO < self + def setup + @csv = CSV.new("h1,h2") + end + + def test_flock + assert_raise(NotImplementedError) do + @csv.flock(File::LOCK_EX) + end + end + + def test_ioctl + assert_raise(NotImplementedError) do + @csv.ioctl(0) + end + end + + def test_stat + assert_raise(NotImplementedError) do + @csv.stat + end + end + + def test_to_i + assert_raise(NotImplementedError) do + @csv.to_i + end + end + + def test_binmode? + assert_equal(false, @csv.binmode?) + end + + def test_path + assert_equal(nil, @csv.path) + end + + def test_to_io + assert_instance_of(StringIO, @csv.to_io) + end + end +end diff --git a/test/csv/interface/test_read.rb b/test/csv/interface/test_read.rb new file mode 100644 index 0000000000..4266ef0578 --- /dev/null +++ b/test/csv/interface/test_read.rb @@ -0,0 +1,381 @@ +# frozen_string_literal: false + +require_relative "../helper" + +class TestCSVInterfaceRead < Test::Unit::TestCase + extend DifferentOFS + + def setup + super + @data = "" + @data << "1\t2\t3\r\n" + @data << "4\t5\r\n" + @input = Tempfile.new(["interface-read", ".csv"], binmode: true) + @input << @data + @input.rewind + @rows = [ + ["1", "2", "3"], + ["4", "5"], + ] + end + + def teardown + @input.close(true) + super + end + + def test_foreach + rows = [] + CSV.foreach(@input.path, col_sep: "\t", row_sep: "\r\n").each do |row| + rows << row + end + assert_equal(@rows, rows) + end + + if respond_to?(:ractor) + ractor + def test_foreach_in_ractor + ractor = Ractor.new(@input.path) do |path| + rows = [] + CSV.foreach(path, col_sep: "\t", row_sep: "\r\n").each do |row| + rows << row + end + rows + end + rows = [ + ["1", "2", "3"], + ["4", "5"], + ] + assert_equal(rows, ractor.take) + end + end + + def test_foreach_mode + rows = [] + CSV.foreach(@input.path, "r", col_sep: "\t", row_sep: "\r\n").each do |row| + rows << row + end + assert_equal(@rows, rows) + end + + def test_foreach_enumurator + rows = CSV.foreach(@input.path, col_sep: "\t", row_sep: "\r\n").to_a + assert_equal(@rows, rows) + end + + def test_closed? + csv = CSV.open(@input.path, "r+", col_sep: "\t", row_sep: "\r\n") + assert_not_predicate(csv, :closed?) + csv.close + assert_predicate(csv, :closed?) + end + + def test_open_auto_close + csv = nil + CSV.open(@input.path) do |_csv| + csv = _csv + end + assert_predicate(csv, :closed?) + end + + def test_open_closed + csv = nil + CSV.open(@input.path) do |_csv| + csv = _csv + csv.close + end + assert_predicate(csv, :closed?) + end + + def test_open_block_return_value + return_value = CSV.open(@input.path) do + "Return value." + end + assert_equal("Return value.", return_value) + end + + def test_open_encoding_valid + # U+1F600 GRINNING FACE + # U+1F601 GRINNING FACE WITH SMILING EYES + File.open(@input.path, "w") do |file| + file << "\u{1F600},\u{1F601}" + end + CSV.open(@input.path, encoding: "utf-8") do |csv| + assert_equal([["\u{1F600}", "\u{1F601}"]], + csv.to_a) + end + end + + def test_open_encoding_invalid + # U+1F600 GRINNING FACE + # U+1F601 GRINNING FACE WITH SMILING EYES + File.open(@input.path, "w") do |file| + file << "\u{1F600},\u{1F601}" + end + CSV.open(@input.path, encoding: "EUC-JP") do |csv| + error = assert_raise(CSV::MalformedCSVError) do + csv.shift + end + assert_equal("Invalid byte sequence in EUC-JP in line 1.", + error.message) + end + end + + def test_open_encoding_nonexistent + _output, error = capture_output do + CSV.open(@input.path, encoding: "nonexistent") do + end + end + assert_equal("path:0: warning: Unsupported encoding nonexistent ignored\n", + error.gsub(/\A.+:\d+: /, "path:0: ")) + end + + def test_open_encoding_utf_8_with_bom + # U+FEFF ZERO WIDTH NO-BREAK SPACE, BOM + # U+1F600 GRINNING FACE + # U+1F601 GRINNING FACE WITH SMILING EYES + File.open(@input.path, "w") do |file| + file << "\u{FEFF}\u{1F600},\u{1F601}" + end + CSV.open(@input.path, encoding: "bom|utf-8") do |csv| + assert_equal([["\u{1F600}", "\u{1F601}"]], + csv.to_a) + end + end + + def test_open_invalid_byte_sequence_in_utf_8 + CSV.open(@input.path, "w", encoding: Encoding::CP932) do |rows| + error = assert_raise(Encoding::InvalidByteSequenceError) do + rows << ["\x82\xa0"] + end + assert_equal('"\x82" on UTF-8', + error.message) + end + end + + def test_open_with_invalid_nil + CSV.open(@input.path, "w", encoding: Encoding::CP932, invalid: nil) do |rows| + error = assert_raise(Encoding::InvalidByteSequenceError) do + rows << ["\x82\xa0"] + end + assert_equal('"\x82" on UTF-8', + error.message) + end + end + + def test_open_with_invalid_replace + CSV.open(@input.path, "w", encoding: Encoding::CP932, invalid: :replace) do |rows| + rows << ["\x82\xa0".force_encoding(Encoding::UTF_8)] + end + CSV.open(@input.path, encoding: Encoding::CP932) do |csv| + assert_equal([["??"]], + csv.to_a) + end + end + + def test_open_with_invalid_replace_and_replace_string + CSV.open(@input.path, "w", encoding: Encoding::CP932, invalid: :replace, replace: "X") do |rows| + rows << ["\x82\xa0".force_encoding(Encoding::UTF_8)] + end + CSV.open(@input.path, encoding: Encoding::CP932) do |csv| + assert_equal([["XX"]], + csv.to_a) + end + end + + def test_open_with_undef_replace + # U+00B7 Middle Dot + CSV.open(@input.path, "w", encoding: Encoding::CP932, undef: :replace) do |rows| + rows << ["\u00B7"] + end + CSV.open(@input.path, encoding: Encoding::CP932) do |csv| + assert_equal([["?"]], + csv.to_a) + end + end + + def test_open_with_undef_replace_and_replace_string + # U+00B7 Middle Dot + CSV.open(@input.path, "w", encoding: Encoding::CP932, undef: :replace, replace: "X") do |rows| + rows << ["\u00B7"] + end + CSV.open(@input.path, encoding: Encoding::CP932) do |csv| + assert_equal([["X"]], + csv.to_a) + end + end + + def test_open_with_newline + CSV.open(@input.path, col_sep: "\t", universal_newline: true) do |csv| + assert_equal(@rows, csv.to_a) + end + File.binwrite(@input.path, "1,2,3\r\n" "4,5\n") + CSV.open(@input.path, newline: :universal) do |csv| + assert_equal(@rows, csv.to_a) + end + end + + def test_parse + assert_equal(@rows, + CSV.parse(@data, col_sep: "\t", row_sep: "\r\n")) + end + + def test_parse_block + rows = [] + CSV.parse(@data, col_sep: "\t", row_sep: "\r\n") do |row| + rows << row + end + assert_equal(@rows, rows) + end + + def test_parse_enumerator + rows = CSV.parse(@data, col_sep: "\t", row_sep: "\r\n").to_a + assert_equal(@rows, rows) + end + + def test_parse_headers_only + table = CSV.parse("a,b,c", headers: true) + assert_equal([ + ["a", "b", "c"], + [], + ], + [ + table.headers, + table.each.to_a, + ]) + end + + def test_parse_line + assert_equal(["1", "2", "3"], + CSV.parse_line("1;2;3", col_sep: ";")) + end + + def test_parse_line_shortcut + assert_equal(["1", "2", "3"], + "1;2;3".parse_csv(col_sep: ";")) + end + + def test_parse_line_empty + assert_equal(nil, CSV.parse_line("")) # to signal eof + end + + def test_parse_line_empty_line + assert_equal([], CSV.parse_line("\n1,2,3")) + end + + def test_read + assert_equal(@rows, + CSV.read(@input.path, col_sep: "\t", row_sep: "\r\n")) + end + + if respond_to?(:ractor) + ractor + def test_read_in_ractor + ractor = Ractor.new(@input.path) do |path| + CSV.read(path, col_sep: "\t", row_sep: "\r\n") + end + rows = [ + ["1", "2", "3"], + ["4", "5"], + ] + assert_equal(rows, ractor.take) + end + end + + def test_readlines + assert_equal(@rows, + CSV.readlines(@input.path, col_sep: "\t", row_sep: "\r\n")) + end + + def test_open_read + rows = CSV.open(@input.path, col_sep: "\t", row_sep: "\r\n") do |csv| + csv.read + end + assert_equal(@rows, rows) + end + + def test_open_readlines + rows = CSV.open(@input.path, col_sep: "\t", row_sep: "\r\n") do |csv| + csv.readlines + end + assert_equal(@rows, rows) + end + + def test_table + table = CSV.table(@input.path, col_sep: "\t", row_sep: "\r\n") + assert_equal(CSV::Table.new([ + CSV::Row.new([:"1", :"2", :"3"], [4, 5, nil]), + ]), + table) + end + + def test_shift # aliased as gets() and readline() + CSV.open(@input.path, "rb+", col_sep: "\t", row_sep: "\r\n") do |csv| + rows = [ + csv.shift, + csv.shift, + csv.shift, + ] + assert_equal(@rows + [nil], + rows) + end + end + + def test_enumerator + CSV.open(@input.path, col_sep: "\t", row_sep: "\r\n") do |csv| + assert_equal(@rows, csv.each.to_a) + end + end + + def test_shift_and_each + CSV.open(@input.path, col_sep: "\t", row_sep: "\r\n") do |csv| + rows = [] + rows << csv.shift + rows.concat(csv.each.to_a) + assert_equal(@rows, rows) + end + end + + def test_each_twice + CSV.open(@input.path, col_sep: "\t", row_sep: "\r\n") do |csv| + assert_equal([ + @rows, + [], + ], + [ + csv.each.to_a, + csv.each.to_a, + ]) + end + end + + def test_eof? + eofs = [] + CSV.open(@input.path, col_sep: "\t", row_sep: "\r\n") do |csv| + eofs << csv.eof? + csv.shift + eofs << csv.eof? + csv.shift + eofs << csv.eof? + end + assert_equal([false, false, true], + eofs) + end + + def test_new_nil + assert_raise_with_message ArgumentError, "Cannot parse nil as CSV" do + CSV.new(nil) + end + end + + def test_options_not_modified + options = {}.freeze + CSV.foreach(@input.path, **options) + CSV.open(@input.path, **options) {} + CSV.parse("", **options) + CSV.parse_line("", **options) + CSV.read(@input.path, **options) + CSV.readlines(@input.path, **options) + CSV.table(@input.path, **options) + end +end diff --git a/test/csv/interface/test_read_write.rb b/test/csv/interface/test_read_write.rb new file mode 100644 index 0000000000..c371e9c5fc --- /dev/null +++ b/test/csv/interface/test_read_write.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: false + +require_relative "../helper" + +class TestCSVInterfaceReadWrite < Test::Unit::TestCase + extend DifferentOFS + + def test_filter + input = <<-CSV.freeze +1;2;3 +4;5 + CSV + output = "" + CSV.filter(input, output, + in_col_sep: ";", + out_col_sep: ",", + converters: :all) do |row| + row.map! {|n| n * 2} + row << "Added\r" + end + assert_equal(<<-CSV, output) +2,4,6,"Added\r" +8,10,"Added\r" + CSV + end + + def test_filter_headers_true + input = <<-CSV.freeze +Name,Value +foo,0 +bar,1 +baz,2 + CSV + output = "" + CSV.filter(input, output, headers: true) do |row| + row[0] += "X" + row[1] = row[1].to_i + 1 + end + assert_equal(<<-CSV, output) +fooX,1 +barX,2 +bazX,3 + CSV + end + + def test_filter_headers_true_write_headers + input = <<-CSV.freeze +Name,Value +foo,0 +bar,1 +baz,2 + CSV + output = "" + CSV.filter(input, output, headers: true, out_write_headers: true) do |row| + if row.is_a?(Array) + row[0] += "X" + row[1] += "Y" + else + row[0] += "X" + row[1] = row[1].to_i + 1 + end + end + assert_equal(<<-CSV, output) +NameX,ValueY +fooX,1 +barX,2 +bazX,3 + CSV + end + + def test_filter_headers_array_write_headers + input = <<-CSV.freeze +foo,0 +bar,1 +baz,2 + CSV + output = "" + CSV.filter(input, output, + headers: ["Name", "Value"], + out_write_headers: true) do |row| + row[0] += "X" + row[1] = row[1].to_i + 1 + end + assert_equal(<<-CSV, output) +Name,Value +fooX,1 +barX,2 +bazX,3 + CSV + end + + def test_instance_same + data = "" + assert_equal(CSV.instance(data, col_sep: ";").object_id, + CSV.instance(data, col_sep: ";").object_id) + end + + def test_instance_append + output = "" + CSV.instance(output, col_sep: ";") << ["a", "b", "c"] + assert_equal(<<-CSV, output) +a;b;c + CSV + CSV.instance(output, col_sep: ";") << [1, 2, 3] + assert_equal(<<-CSV, output) +a;b;c +1;2;3 + CSV + end + + def test_instance_shortcut + assert_equal(CSV.instance, + CSV {|csv| csv}) + end + + def test_instance_shortcut_with_io + io = StringIO.new + from_instance = CSV.instance(io, col_sep: ";") { |csv| csv << ["a", "b", "c"] } + from_shortcut = CSV(io, col_sep: ";") { |csv| csv << ["e", "f", "g"] } + + assert_equal(from_instance, from_shortcut) + assert_equal(from_instance.string, "a;b;c\ne;f;g\n") + end +end diff --git a/test/csv/interface/test_write.rb b/test/csv/interface/test_write.rb new file mode 100644 index 0000000000..0cd39a7663 --- /dev/null +++ b/test/csv/interface/test_write.rb @@ -0,0 +1,217 @@ +# frozen_string_literal: false + +require_relative "../helper" + +class TestCSVInterfaceWrite < Test::Unit::TestCase + extend DifferentOFS + + def setup + super + @output = Tempfile.new(["interface-write", ".csv"]) + end + + def teardown + @output.close(true) + super + end + + def test_generate_default + csv_text = CSV.generate do |csv| + csv << [1, 2, 3] << [4, nil, 5] + end + assert_equal(<<-CSV, csv_text) +1,2,3 +4,,5 + CSV + end + + if respond_to?(:ractor) + ractor + def test_generate_default_in_ractor + ractor = Ractor.new do + CSV.generate do |csv| + csv << [1, 2, 3] << [4, nil, 5] + end + end + assert_equal(<<-CSV, ractor.take) +1,2,3 +4,,5 + CSV + end + end + + def test_generate_append + csv_text = <<-CSV +1,2,3 +4,,5 + CSV + CSV.generate(csv_text) do |csv| + csv << ["last", %Q{"row"}] + end + assert_equal(<<-CSV, csv_text) +1,2,3 +4,,5 +last,"""row""" + CSV + end + + def test_generate_no_new_line + csv_text = CSV.generate("test") do |csv| + csv << ["row"] + end + assert_equal(<<-CSV, csv_text) +testrow + CSV + end + + def test_generate_line_col_sep + line = CSV.generate_line(["1", "2", "3"], col_sep: ";") + assert_equal(<<-LINE, line) +1;2;3 + LINE + end + + def test_generate_line_row_sep + line = CSV.generate_line(["1", "2"], row_sep: nil) + assert_equal(<<-LINE.chomp, line) +1,2 + LINE + end + + def test_generate_line_shortcut + line = ["1", "2", "3"].to_csv(col_sep: ";") + assert_equal(<<-LINE, line) +1;2;3 + LINE + end + + def test_generate_lines + lines = CSV.generate_lines([["foo", "bar"], [1, 2], [3, 4]]) + assert_equal(<<-LINES, lines) +foo,bar +1,2 +3,4 + LINES + end + + def test_headers_detection + headers = ["a", "b", "c"] + CSV.open(@output.path, "w", headers: true) do |csv| + csv << headers + csv << ["1", "2", "3"] + assert_equal(headers, csv.headers) + end + end + + def test_lineno + CSV.open(@output.path, "w") do |csv| + n_lines = 20 + n_lines.times do + csv << ["a", "b", "c"] + end + assert_equal(n_lines, csv.lineno) + end + end + + def test_append_row + CSV.open(@output.path, "wb") do |csv| + csv << + CSV::Row.new([], ["1", "2", "3"]) << + CSV::Row.new([], ["a", "b", "c"]) + end + assert_equal(<<-CSV, File.read(@output.path, mode: "rb")) +1,2,3 +a,b,c + CSV + end + + + if respond_to?(:ractor) + ractor + def test_append_row_in_ractor + ractor = Ractor.new(@output.path) do |path| + CSV.open(path, "wb") do |csv| + csv << + CSV::Row.new([], ["1", "2", "3"]) << + CSV::Row.new([], ["a", "b", "c"]) + end + end + ractor.take + assert_equal(<<-CSV, File.read(@output.path, mode: "rb")) +1,2,3 +a,b,c + CSV + end + end + + def test_append_hash + CSV.open(@output.path, "wb", headers: true) do |csv| + csv << [:a, :b, :c] + csv << {a: 1, b: 2, c: 3} + csv << {a: 4, b: 5, c: 6} + end + assert_equal(<<-CSV, File.read(@output.path, mode: "rb")) +a,b,c +1,2,3 +4,5,6 + CSV + end + + def test_append_hash_headers_array + CSV.open(@output.path, "wb", headers: [:b, :a, :c]) do |csv| + csv << {a: 1, b: 2, c: 3} + csv << {a: 4, b: 5, c: 6} + end + assert_equal(<<-CSV, File.read(@output.path, mode: "rb")) +2,1,3 +5,4,6 + CSV + end + + def test_append_hash_headers_string + CSV.open(@output.path, "wb", headers: "b|a|c", col_sep: "|") do |csv| + csv << {"a" => 1, "b" => 2, "c" => 3} + csv << {"a" => 4, "b" => 5, "c" => 6} + end + assert_equal(<<-CSV, File.read(@output.path, mode: "rb")) +2|1|3 +5|4|6 + CSV + end + + def test_write_headers + CSV.open(@output.path, + "wb", + headers: "b|a|c", + write_headers: true, + col_sep: "|" ) do |csv| + csv << {"a" => 1, "b" => 2, "c" => 3} + csv << {"a" => 4, "b" => 5, "c" => 6} + end + assert_equal(<<-CSV, File.read(@output.path, mode: "rb")) +b|a|c +2|1|3 +5|4|6 + CSV + end + + def test_write_headers_empty + CSV.open(@output.path, + "wb", + headers: "b|a|c", + write_headers: true, + col_sep: "|" ) do |csv| + end + assert_equal(<<-CSV, File.read(@output.path, mode: "rb")) +b|a|c + CSV + end + + def test_options_not_modified + options = {}.freeze + CSV.generate(**options) {} + CSV.generate_line([], **options) + CSV.filter("", "", **options) + CSV.instance("", **options) + end +end diff --git a/test/csv/parse/test_column_separator.rb b/test/csv/parse/test_column_separator.rb new file mode 100644 index 0000000000..d6eaa7b6de --- /dev/null +++ b/test/csv/parse/test_column_separator.rb @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# frozen_string_literal: false + +require_relative "../helper" + +class TestCSVParseColumnSeparator < Test::Unit::TestCase + extend DifferentOFS + + def test_comma + assert_equal([["a", "b", nil, "d"]], + CSV.parse("a,b,,d", col_sep: ",")) + end + + def test_space + assert_equal([["a", "b", nil, "d"]], + CSV.parse("a b d", col_sep: " ")) + end + + def test_tab + assert_equal([["a", "b", nil, "d"]], + CSV.parse("a\tb\t\td", col_sep: "\t")) + end + + def test_multiple_characters_include_sub_separator + assert_equal([["a b", nil, "d"]], + CSV.parse("a b d", col_sep: " ")) + end + + def test_multiple_characters_leading_empty_fields + data = <<-CSV +<=><=>A<=>B<=>C +1<=>2<=>3 + CSV + assert_equal([ + [nil, nil, "A", "B", "C"], + ["1", "2", "3"], + ], + CSV.parse(data, col_sep: "<=>")) + end +end diff --git a/test/csv/parse/test_convert.rb b/test/csv/parse/test_convert.rb new file mode 100644 index 0000000000..c9195c71d9 --- /dev/null +++ b/test/csv/parse/test_convert.rb @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +# frozen_string_literal: false + +require_relative "../helper" + +class TestCSVParseConvert < Test::Unit::TestCase + extend DifferentOFS + + def setup + super + @data = "Numbers,:integer,1,:float,3.015" + @parser = CSV.new(@data) + + @custom = lambda {|field| /\A:(\S.*?)\s*\Z/ =~ field ? $1.to_sym : field} + + @time = Time.utc(2018, 12, 30, 6, 41, 29) + @windows_safe_time_data = @time.strftime("%a %b %d %H:%M:%S %Y") + + @preserving_converter = lambda do |field, info| + f = field.encode(CSV::ConverterEncoding) + return f if info.quoted? + begin + Integer(f, 10) + rescue + f + end + end + + @quoted_header_converter = lambda do |field, info| + f = field.encode(CSV::ConverterEncoding) + return f if info.quoted? + f.to_sym + end + end + + def test_integer + @parser.convert(:integer) + assert_equal(["Numbers", ":integer", 1, ":float", "3.015"], + @parser.shift) + end + + def test_float + @parser.convert(:float) + assert_equal(["Numbers", ":integer", 1.0, ":float", 3.015], + @parser.shift) + end + + def test_float_integer + @parser.convert(:float) + @parser.convert(:integer) + assert_equal(["Numbers", ":integer", 1.0, ":float", 3.015], + @parser.shift) + end + + def test_integer_float + @parser.convert(:integer) + @parser.convert(:float) + assert_equal(["Numbers", ":integer", 1, ":float", 3.015], + @parser.shift) + end + + def test_numeric + @parser.convert(:numeric) + assert_equal(["Numbers", ":integer", 1, ":float", 3.015], + @parser.shift) + end + + def test_all + @data << ",#{@windows_safe_time_data}" + @parser = CSV.new(@data) + @parser.convert(:all) + assert_equal(["Numbers", ":integer", 1, ":float", 3.015, @time.to_datetime], + @parser.shift) + end + + def test_custom + @parser.convert do |field| + /\A:(\S.*?)\s*\Z/ =~ field ? $1.to_sym : field + end + assert_equal(["Numbers", :integer, "1", :float, "3.015"], + @parser.shift) + end + + def test_builtin_custom + @parser.convert(:numeric) + @parser.convert(&@custom) + assert_equal(["Numbers", :integer, 1, :float, 3.015], + @parser.shift) + end + + def test_custom_field_info_line + @parser.convert do |field, info| + assert_equal(1, info.line) + info.index == 4 ? Float(field).floor : field + end + assert_equal(["Numbers", ":integer", "1", ":float", 3], + @parser.shift) + end + + def test_custom_field_info_header + headers = ["one", "two", "three", "four", "five"] + @parser = CSV.new(@data, headers: headers) + @parser.convert do |field, info| + info.header == "three" ? Integer(field) * 100 : field + end + assert_equal(CSV::Row.new(headers, + ["Numbers", ":integer", 100, ":float", "3.015"]), + @parser.shift) + end + + def test_custom_blank_field + converter = lambda {|field| field.nil?} + row = CSV.parse_line('nil,', converters: converter) + assert_equal([false, true], row) + end + + def test_nil_value + assert_equal(["nil", "", "a"], + CSV.parse_line(',"",a', nil_value: "nil")) + end + + def test_empty_value + assert_equal([nil, "empty", "a"], + CSV.parse_line(',"",a', empty_value: "empty")) + end + + def test_quoted_parse_line + row = CSV.parse_line('1,"2",3', converters: @preserving_converter) + assert_equal([1, "2", 3], row) + end + + def test_quoted_parse + expected = [["quoted", "unquoted"], ["109", 1], ["10A", 2]] + rows = CSV.parse(<<~CSV, converters: @preserving_converter) + "quoted",unquoted + "109",1 + "10A",2 + CSV + assert_equal(expected, rows) + end + + def test_quoted_alternating_quote + row = CSV.parse_line('"1",2,"3"', converters: @preserving_converter) + assert_equal(['1', 2, '3'], row) + end + + def test_quoted_parse_headers + expected = [["quoted", :unquoted], ["109", "1"], ["10A", "2"]] + table = CSV.parse(<<~CSV, headers: true, header_converters: @quoted_header_converter) + "quoted",unquoted + "109",1 + "10A",2 + CSV + assert_equal(expected, table.to_a) + end + + def test_quoted_parse_with_string_headers + expected = [["quoted", :unquoted], %w[109 1], %w[10A 2]] + table = CSV.parse(<<~CSV, headers: '"quoted",unquoted', header_converters: @quoted_header_converter) + "109",1 + "10A",2 + CSV + assert_equal(expected, table.to_a) + end +end diff --git a/test/csv/parse/test_each.rb b/test/csv/parse/test_each.rb new file mode 100644 index 0000000000..ce0b71d058 --- /dev/null +++ b/test/csv/parse/test_each.rb @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# frozen_string_literal: false + +require_relative "../helper" + +class TestCSVParseEach < Test::Unit::TestCase + extend DifferentOFS + + def test_twice + data = <<-CSV +Ruby,2.6.0,script + CSV + csv = CSV.new(data) + assert_equal([ + [["Ruby", "2.6.0", "script"]], + [], + ], + [ + csv.to_a, + csv.to_a, + ]) + end +end diff --git a/test/csv/test_csv_parsing.rb b/test/csv/parse/test_general.rb index 319f3f392f..902be2ce4a 100755..100644 --- a/test/csv/test_csv_parsing.rb +++ b/test/csv/parse/test_general.rb @@ -1,15 +1,9 @@ -#!/usr/bin/env ruby -w -# encoding: UTF-8 - -# tc_csv_parsing.rb -# -# Created by James Edward Gray II on 2005-10-31. -# Copyright 2005 James Edward Gray II. You can redistribute or modify this code -# under the terms of Ruby's license. +# -*- coding: utf-8 -*- +# frozen_string_literal: false require "timeout" -require_relative "base" +require_relative "../helper" # # Following tests are my interpretation of the @@ -17,10 +11,10 @@ require_relative "base" # document in one place (intentionally) and that is to make the default row # separator <tt>$/</tt>. # -class TestCSV::Parsing < TestCSV +class TestCSVParseGeneral < Test::Unit::TestCase extend DifferentOFS - BIG_DATA = "123456789\n" * 1024 + BIG_DATA = "123456789\n" * 512 def test_mastering_regex_example ex = %Q{Ten Thousand,10000, 2710 ,,"10,000","It's ""10 Grand"", baby",10K} @@ -144,55 +138,52 @@ class TestCSV::Parsing < TestCSV end end - def test_malformed_csv - assert_raise(CSV::MalformedCSVError) do + def test_malformed_csv_cr_first_line + error = assert_raise(CSV::MalformedCSVError) do CSV.parse_line("1,2\r,3", row_sep: "\n") end + assert_equal("Unquoted fields do not allow new line <\"\\r\"> in line 1.", + error.message) + end - bad_data = <<-END_DATA.gsub(/^ +/, "") - line,1,abc - line,2,"def\nghi" - - line,4,some\rjunk - line,5,jkl - END_DATA - lines = bad_data.lines.to_a - assert_equal(6, lines.size) - assert_match(/\Aline,4/, lines.find { |l| l =~ /some\rjunk/ }) - - csv = CSV.new(bad_data) - begin - loop do - assert_not_nil(csv.shift) - assert_send([csv.lineno, :<, 4]) - end - rescue CSV::MalformedCSVError - assert_equal( "Unquoted fields do not allow \\r or \\n (line 4).", - $!.message ) + def test_malformed_csv_cr_middle_line + csv = <<-CSV +line,1,abc +line,2,"def\nghi" + +line,4,some\rjunk +line,5,jkl + CSV + + error = assert_raise(CSV::MalformedCSVError) do + CSV.parse(csv) end + assert_equal("Unquoted fields do not allow new line <\"\\r\"> in line 4.", + error.message) + end - assert_raise(CSV::MalformedCSVError) { CSV.parse_line('1,2,"3...') } + def test_malformed_csv_unclosed_quote + error = assert_raise(CSV::MalformedCSVError) do + CSV.parse_line('1,2,"3...') + end + assert_equal("Unclosed quoted field in line 1.", + error.message) + end - bad_data = <<-END_DATA.gsub(/^ +/, "") - line,1,abc - line,2,"def\nghi" + def test_malformed_csv_illegal_quote_middle_line + csv = <<-CSV +line,1,abc +line,2,"def\nghi" - line,4,8'10" - line,5,jkl - END_DATA - lines = bad_data.lines.to_a - assert_equal(6, lines.size) - assert_match(/\Aline,4/, lines.find { |l| l =~ /8'10"/ }) +line,4,8'10" +line,5,jkl + CSV - csv = CSV.new(bad_data) - begin - loop do - assert_not_nil(csv.shift) - assert_send([csv.lineno, :<, 4]) - end - rescue CSV::MalformedCSVError - assert_equal("Illegal quoting in line 4.", $!.message) + error = assert_raise(CSV::MalformedCSVError) do + CSV.parse(csv) end + assert_equal("Illegal quoting in line 4.", + error.message) end def test_the_parse_fails_fast_when_it_can_for_unquoted_fields @@ -208,12 +199,134 @@ class TestCSV::Parsing < TestCSV field_size_limit: 2048 ) end - private + def test_field_size_limit_max_allowed + column = "abcde" + assert_equal([[column]], + CSV.parse("\"#{column}\"", + field_size_limit: column.size + 1)) + end - def assert_parse_errors_out(*args) + def test_field_size_limit_quote_simple + column = "abcde" + assert_parse_errors_out("\"#{column}\"", + field_size_limit: column.size) + end + + def test_field_size_limit_no_quote_implicitly + column = "abcde" + assert_parse_errors_out("#{column}", + field_size_limit: column.size) + end + + def test_field_size_limit_no_quote_explicitly + column = "abcde" + assert_parse_errors_out("#{column}", + field_size_limit: column.size, + quote_char: nil) + end + + def test_field_size_limit_in_extended_column_not_exceeding + data = <<~DATA + "a","b" + " + 2 + ","" + DATA + assert_nothing_raised(CSV::MalformedCSVError) do + CSV.parse(data, field_size_limit: 4) + end + end + + def test_field_size_limit_in_extended_column_exceeding + data = <<~DATA + "a","b" + " + 2345 + ","" + DATA + assert_parse_errors_out(data, field_size_limit: 5) + end + + def test_max_field_size_controls_lookahead + assert_parse_errors_out( 'valid,fields,"' + BIG_DATA + '"', + max_field_size: 2048 ) + end + + def test_max_field_size_max_allowed + column = "abcde" + assert_equal([[column]], + CSV.parse("\"#{column}\"", + max_field_size: column.size)) + end + + def test_max_field_size_quote_simple + column = "abcde" + assert_parse_errors_out("\"#{column}\"", + max_field_size: column.size - 1) + end + + def test_max_field_size_no_quote_implicitly + column = "abcde" + assert_parse_errors_out("#{column}", + max_field_size: column.size - 1) + end + + def test_max_field_size_no_quote_explicitly + column = "abcde" + assert_parse_errors_out("#{column}", + max_field_size: column.size - 1, + quote_char: nil) + end + + def test_max_field_size_in_extended_column_not_exceeding + data = <<~DATA + "a","b" + " + 2 + ","" + DATA + assert_nothing_raised(CSV::MalformedCSVError) do + CSV.parse(data, max_field_size: 3) + end + end + + def test_max_field_size_in_extended_column_exceeding + data = <<~DATA + "a","b" + " + 2345 + ","" + DATA + assert_parse_errors_out(data, max_field_size: 4) + end + + def test_row_sep_auto_cr + assert_equal([["a"]], CSV.parse("a\r")) + end + + def test_row_sep_auto_lf + assert_equal([["a"]], CSV.parse("a\n")) + end + + def test_row_sep_auto_cr_lf + assert_equal([["a"]], CSV.parse("a\r\n")) + end + + def test_seeked_string_io + input_with_bom = StringIO.new("\ufeffあ,い,う\r\na,b,c\r\n") + input_with_bom.read(3) + assert_equal([ + ["あ", "い", "う"], + ["a", "b", "c"], + ], + CSV.new(input_with_bom).each.to_a) + end + + private + def assert_parse_errors_out(data, **options) assert_raise(CSV::MalformedCSVError) do Timeout.timeout(0.2) do - CSV.parse(*args) + CSV.parse(data, **options) fail("Parse didn't error out") end end diff --git a/test/csv/test_headers.rb b/test/csv/parse/test_header.rb index 79ccd20c74..e8c3786d68 100755..100644 --- a/test/csv/test_headers.rb +++ b/test/csv/parse/test_header.rb @@ -1,24 +1,18 @@ -#!/usr/bin/env ruby -w -# encoding: UTF-8 +# -*- coding: utf-8 -*- +# frozen_string_literal: false -# tc_headers.rb -# -# Created by James Edward Gray II on 2005-10-31. -# Copyright 2005 James Edward Gray II. You can redistribute or modify this code -# under the terms of Ruby's license. +require_relative "../helper" -require_relative "base" - -class TestCSV::Headers < TestCSV +class TestCSVHeaders < Test::Unit::TestCase extend DifferentOFS def setup super - @data = <<-END_CSV.gsub(/^\s+/, "") - first,second,third - A,B,C - 1,2,3 - END_CSV + @data = <<-CSV +first,second,third +A,B,C +1,2,3 + CSV end def test_first_row @@ -184,10 +178,10 @@ class TestCSV::Headers < TestCSV def test_converters # create test data where headers and fields look alike - data = <<-END_MATCHING_CSV.gsub(/^\s+/, "") - 1,2,3 - 1,2,3 - END_MATCHING_CSV + data = <<-CSV +1,2,3 +1,2,3 + CSV # normal converters do not affect headers csv = CSV.parse( data, headers: true, @@ -224,10 +218,24 @@ class TestCSV::Headers < TestCSV assert_equal([:one, :two_three], csv.headers) end + def test_builtin_symbol_raw_converter + csv = CSV.parse( "a b,c d", headers: true, + return_headers: true, + header_converters: :symbol_raw ) + assert_equal([:"a b", :"c d"], csv.headers) + end + + def test_builtin_symbol_converter_with_punctuation + csv = CSV.parse( "One, Two & Three ($)", headers: true, + return_headers: true, + header_converters: :symbol ) + assert_equal([:one, :two_three], csv.headers) + end + def test_builtin_converters_with_blank_header csv = CSV.parse( "one,,three", headers: true, return_headers: true, - header_converters: [:downcase, :symbol] ) + header_converters: [:downcase, :symbol, :symbol_raw] ) assert_equal([:one, nil, :three], csv.headers) end @@ -250,16 +258,16 @@ class TestCSV::Headers < TestCSV end def test_skip_blanks - @data = <<-END_CSV.gsub(/^ +/, "") + @data = <<-CSV - A,B,C +A,B,C - 1,2,3 +1,2,3 - END_CSV + CSV expected = [%w[1 2 3]] CSV.parse(@data, headers: true, skip_blanks: true) do |row| @@ -286,7 +294,7 @@ class TestCSV::Headers < TestCSV assert_equal(%w[first second third], csv.headers) # after headers are read end - def test_blank_row_bug_fix + def test_blank_row @data += "\n#{@data}" # add a blank row # ensure that everything returned is a Row object @@ -294,4 +302,41 @@ class TestCSV::Headers < TestCSV assert_instance_of(CSV::Row, row) end end + + def test_nil_row_header + @data = <<-CSV +A + +1 + CSV + + csv = CSV.parse(@data, headers: true) + + # ensure nil row creates Row object with headers + row = csv[0] + assert_equal([["A"], [nil]], + [row.headers, row.fields]) + end + + def test_parse_empty + assert_equal(CSV::Table.new([]), + CSV.parse("", headers: true)) + end + + def test_parse_empty_line + assert_equal(CSV::Table.new([]), + CSV.parse("\n", headers: true)) + end + + def test_specified_empty + assert_equal(CSV::Table.new([], + headers: ["header1"]), + CSV.parse("", headers: ["header1"])) + end + + def test_specified_empty_line + assert_equal(CSV::Table.new([CSV::Row.new(["header1"], [])], + headers: ["header1"]), + CSV.parse("\n", headers: ["header1"])) + end end diff --git a/test/csv/parse/test_inputs_scanner.rb b/test/csv/parse/test_inputs_scanner.rb new file mode 100644 index 0000000000..06e1c845d5 --- /dev/null +++ b/test/csv/parse/test_inputs_scanner.rb @@ -0,0 +1,63 @@ +require_relative "../helper" + +class TestCSVParseInputsScanner < Test::Unit::TestCase + include Helper + + def test_scan_keep_over_chunks_nested_back + input = CSV::Parser::UnoptimizedStringIO.new("abcdefghijklmnl") + scanner = CSV::Parser::InputsScanner.new([input], + Encoding::UTF_8, + nil, + chunk_size: 2) + scanner.keep_start + assert_equal("abc", scanner.scan_all(/[a-c]+/)) + scanner.keep_start + assert_equal("def", scanner.scan_all(/[d-f]+/)) + scanner.keep_back + scanner.keep_back + assert_equal("abcdefg", scanner.scan_all(/[a-g]+/)) + end + + def test_scan_keep_over_chunks_nested_drop_back + input = CSV::Parser::UnoptimizedStringIO.new("abcdefghijklmnl") + scanner = CSV::Parser::InputsScanner.new([input], + Encoding::UTF_8, + nil, + chunk_size: 3) + scanner.keep_start + assert_equal("ab", scanner.scan(/../)) + scanner.keep_start + assert_equal("c", scanner.scan(/./)) + assert_equal("d", scanner.scan(/./)) + scanner.keep_drop + scanner.keep_back + assert_equal("abcdefg", scanner.scan_all(/[a-g]+/)) + end + + def test_each_line_keep_over_chunks_multibyte + input = CSV::Parser::UnoptimizedStringIO.new("ab\n\u{3000}a\n") + scanner = CSV::Parser::InputsScanner.new([input], + Encoding::UTF_8, + nil, + chunk_size: 1) + each_line = scanner.each_line("\n") + assert_equal("ab\n", each_line.next) + scanner.keep_start + assert_equal("\u{3000}a\n", each_line.next) + scanner.keep_back + assert_equal("\u{3000}a\n", scanner.scan_all(/[^,]+/)) + end + + def test_each_line_keep_over_chunks_fit_chunk_size + input = CSV::Parser::UnoptimizedStringIO.new("\na") + scanner = CSV::Parser::InputsScanner.new([input], + Encoding::UTF_8, + nil, + chunk_size: 1) + each_line = scanner.each_line("\n") + assert_equal("\n", each_line.next) + scanner.keep_start + assert_equal("a", each_line.next) + scanner.keep_back + end +end diff --git a/test/csv/parse/test_invalid.rb b/test/csv/parse/test_invalid.rb new file mode 100644 index 0000000000..ddb59e2b9a --- /dev/null +++ b/test/csv/parse/test_invalid.rb @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# frozen_string_literal: false + +require_relative "../helper" + +class TestCSVParseInvalid < Test::Unit::TestCase + def test_no_column_mixed_new_lines + error = assert_raise(CSV::MalformedCSVError) do + CSV.parse("\n" + + "\r") + end + assert_equal("New line must be <\"\\n\"> not <\"\\r\"> in line 2.", + error.message) + end + + def test_ignore_invalid_line + csv = CSV.new(<<-CSV, headers: true, return_headers: true) +head1,head2,head3 +aaa,bbb,ccc +ddd,ee"e.fff +ggg,hhh,iii + CSV + headers = ["head1", "head2", "head3"] + assert_equal(CSV::Row.new(headers, headers), + csv.shift) + assert_equal(CSV::Row.new(headers, ["aaa", "bbb", "ccc"]), + csv.shift) + assert_equal(false, csv.eof?) + error = assert_raise(CSV::MalformedCSVError) do + csv.shift + end + assert_equal("Illegal quoting in line 3.", + error.message) + assert_equal(false, csv.eof?) + assert_equal(CSV::Row.new(headers, ["ggg", "hhh", "iii"]), + csv.shift) + assert_equal(true, csv.eof?) + end + + def test_ignore_invalid_line_cr_lf + data = <<-CSV +"1","OK"\r +"2",""NOT" OK"\r +"3","OK"\r +CSV + csv = CSV.new(data) + + assert_equal(['1', 'OK'], csv.shift) + assert_raise(CSV::MalformedCSVError) { csv.shift } + assert_equal(['3', 'OK'], csv.shift) + end +end diff --git a/test/csv/parse/test_liberal_parsing.rb b/test/csv/parse/test_liberal_parsing.rb new file mode 100644 index 0000000000..5796d10828 --- /dev/null +++ b/test/csv/parse/test_liberal_parsing.rb @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +# frozen_string_literal: false + +require_relative "../helper" + +class TestCSVParseLiberalParsing < Test::Unit::TestCase + extend DifferentOFS + + def test_middle_quote_start + input = '"Johnson, Dwayne",Dwayne "The Rock" Johnson' + error = assert_raise(CSV::MalformedCSVError) do + CSV.parse_line(input) + end + assert_equal("Illegal quoting in line 1.", + error.message) + assert_equal(["Johnson, Dwayne", 'Dwayne "The Rock" Johnson'], + CSV.parse_line(input, liberal_parsing: true)) + end + + def test_middle_quote_end + input = '"quoted" field' + error = assert_raise(CSV::MalformedCSVError) do + CSV.parse_line(input) + end + assert_equal("Any value after quoted field isn't allowed in line 1.", + error.message) + assert_equal(['"quoted" field'], + CSV.parse_line(input, liberal_parsing: true)) + end + + def test_endline_after_quoted_field_end + csv = CSV.new("A\r\n\"B\"\nC\r\n", liberal_parsing: true) + assert_equal(["A"], csv.gets) + error = assert_raise(CSV::MalformedCSVError) do + csv.gets + end + assert_equal('Illegal end-of-line sequence outside of a quoted field <"\n"> in line 2.', + error.message) + assert_equal(["C"], csv.gets) + end + + def test_quote_after_column_separator + error = assert_raise(CSV::MalformedCSVError) do + CSV.parse_line('is,this "three," or four,fields', liberal_parsing: true) + end + assert_equal("Unclosed quoted field in line 1.", + error.message) + end + + def test_quote_before_column_separator + assert_equal(["is", 'this "three', ' or four"', "fields"], + CSV.parse_line('is,this "three, or four",fields', + liberal_parsing: true)) + end + + def test_backslash_quote + assert_equal([ + "1", + "\"Hamlet says, \\\"Seems", + "\\\" madam! Nay it is; I know not \\\"seems.\\\"\"", + ], + CSV.parse_line('1,' + + '"Hamlet says, \"Seems,' + + '\" madam! Nay it is; I know not \"seems.\""', + liberal_parsing: true)) + end + + def test_space_quote + input = <<~CSV + Los Angeles, 34°03'N, 118°15'W + New York City, 40°42'46"N, 74°00'21"W + Paris, 48°51'24"N, 2°21'03"E + CSV + assert_equal( + [ + ["Los Angeles", " 34°03'N", " 118°15'W"], + ["New York City", " 40°42'46\"N", " 74°00'21\"W"], + ["Paris", " 48°51'24\"N", " 2°21'03\"E"], + ], + CSV.parse(input, liberal_parsing: true)) + end + + def test_double_quote_outside_quote + data = %Q{a,""b""} + error = assert_raise(CSV::MalformedCSVError) do + CSV.parse(data) + end + assert_equal("Any value after quoted field isn't allowed in line 1.", + error.message) + assert_equal([ + [["a", %Q{""b""}]], + [["a", %Q{"b"}]], + ], + [ + CSV.parse(data, liberal_parsing: true), + CSV.parse(data, + liberal_parsing: { + double_quote_outside_quote: true, + }), + ]) + end + + class TestBackslashQuote < Test::Unit::TestCase + extend ::DifferentOFS + + def test_double_quote_outside_quote + data = %Q{a,""b""} + assert_equal([ + [["a", %Q{""b""}]], + [["a", %Q{"b"}]], + ], + [ + CSV.parse(data, + liberal_parsing: { + backslash_quote: true + }), + CSV.parse(data, + liberal_parsing: { + backslash_quote: true, + double_quote_outside_quote: true + }), + ]) + end + + def test_unquoted_value + data = %q{\"\"a\"\"} + assert_equal([ + [[%q{\"\"a\"\"}]], + [[%q{""a""}]], + ], + [ + CSV.parse(data, liberal_parsing: true), + CSV.parse(data, + liberal_parsing: { + backslash_quote: true + }), + ]) + end + + def test_unquoted_value_multiple_characters_col_sep + data = %q{a<\\"b<=>x} + assert_equal([[%Q{a<"b}, "x"]], + CSV.parse(data, + col_sep: "<=>", + liberal_parsing: { + backslash_quote: true + })) + end + + def test_quoted_value + data = %q{"\"\"a\"\""} + assert_equal([ + [[%q{"\"\"a\"\""}]], + [[%q{""a""}]], + [[%q{""a""}]], + ], + [ + CSV.parse(data, liberal_parsing: true), + CSV.parse(data, + liberal_parsing: { + backslash_quote: true + }), + CSV.parse(data, + liberal_parsing: { + backslash_quote: true, + double_quote_outside_quote: true + }), + ]) + end + end +end diff --git a/test/csv/parse/test_quote_char_nil.rb b/test/csv/parse/test_quote_char_nil.rb new file mode 100644 index 0000000000..fc3b646759 --- /dev/null +++ b/test/csv/parse/test_quote_char_nil.rb @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# frozen_string_literal: false + +require_relative "../helper" + +class TestCSVParseQuoteCharNil < Test::Unit::TestCase + extend DifferentOFS + + def test_full + assert_equal(["a", "b"], CSV.parse_line(%Q{a,b}, quote_char: nil)) + end + + def test_end_with_nil + assert_equal(["a", nil, nil, nil], CSV.parse_line(%Q{a,,,}, quote_char: nil)) + end + + def test_nil_nil + assert_equal([nil, nil], CSV.parse_line(%Q{,}, quote_char: nil)) + end + + def test_unquoted_value_multiple_characters_col_sep + data = %q{a<b<=>x} + assert_equal([[%Q{a<b}, "x"]], CSV.parse(data, col_sep: "<=>", quote_char: nil)) + end + + def test_csv_header_string + data = <<~DATA + first,second,third + A,B,C + 1,2,3 + DATA + assert_equal( + CSV::Table.new([ + CSV::Row.new(["my", "new", "headers"], ["first", "second", "third"]), + CSV::Row.new(["my", "new", "headers"], ["A", "B", "C"]), + CSV::Row.new(["my", "new", "headers"], ["1", "2", "3"]) + ]), + CSV.parse(data, headers: "my,new,headers", quote_char: nil) + ) + end + + def test_comma + assert_equal([["a", "b", nil, "d"]], + CSV.parse("a,b,,d", col_sep: ",", quote_char: nil)) + end + + def test_space + assert_equal([["a", "b", nil, "d"]], + CSV.parse("a b d", col_sep: " ", quote_char: nil)) + end + + def encode_array(array, encoding) + array.collect do |element| + element ? element.encode(encoding) : element + end + end + + def test_space_no_ascii + encoding = Encoding::UTF_16LE + assert_equal([encode_array(["a", "b", nil, "d"], encoding)], + CSV.parse("a b d".encode(encoding), + col_sep: " ".encode(encoding), + quote_char: nil)) + end + + def test_multiple_space + assert_equal([["a b", nil, "d"]], + CSV.parse("a b d", col_sep: " ", quote_char: nil)) + end + + def test_multiple_characters_leading_empty_fields + data = <<-CSV +<=><=>A<=>B<=>C +1<=>2<=>3 + CSV + assert_equal([ + [nil, nil, "A", "B", "C"], + ["1", "2", "3"], + ], + CSV.parse(data, col_sep: "<=>", quote_char: nil)) + end + + def test_line + lines = [ + "abc,def\n", + ] + csv = CSV.new(lines.join(""), quote_char: nil) + lines.each do |line| + csv.shift + assert_equal(line, csv.line) + end + end +end diff --git a/test/csv/parse/test_rewind.rb b/test/csv/parse/test_rewind.rb new file mode 100644 index 0000000000..0aa403b756 --- /dev/null +++ b/test/csv/parse/test_rewind.rb @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# frozen_string_literal: false + +require_relative "../helper" + +class TestCSVParseRewind < Test::Unit::TestCase + extend DifferentOFS + + def parse(data, **options) + csv = CSV.new(data, **options) + records = csv.to_a + csv.rewind + [records, csv.to_a] + end + + def test_default + data = <<-CSV +Ruby,2.6.0,script + CSV + assert_equal([ + [["Ruby", "2.6.0", "script"]], + [["Ruby", "2.6.0", "script"]], + ], + parse(data)) + end + + def test_have_headers + data = <<-CSV +Language,Version,Type +Ruby,2.6.0,script + CSV + assert_equal([ + [CSV::Row.new(["Language", "Version", "Type"], + ["Ruby", "2.6.0", "script"])], + [CSV::Row.new(["Language", "Version", "Type"], + ["Ruby", "2.6.0", "script"])], + ], + parse(data, headers: true)) + end +end diff --git a/test/csv/parse/test_row_separator.rb b/test/csv/parse/test_row_separator.rb new file mode 100644 index 0000000000..eaf6adc910 --- /dev/null +++ b/test/csv/parse/test_row_separator.rb @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# frozen_string_literal: false + +require_relative "../helper" + +class TestCSVParseRowSeparator < Test::Unit::TestCase + extend DifferentOFS + include Helper + + def test_multiple_characters + with_chunk_size("1") do + assert_equal([["a"], ["b"]], + CSV.parse("a\r\nb\r\n", row_sep: "\r\n")) + end + end +end diff --git a/test/csv/parse/test_skip_lines.rb b/test/csv/parse/test_skip_lines.rb new file mode 100644 index 0000000000..98d67ae51c --- /dev/null +++ b/test/csv/parse/test_skip_lines.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: false + +require_relative "../helper" + +class TestCSVParseSkipLines < Test::Unit::TestCase + extend DifferentOFS + include Helper + + def test_default + csv = CSV.new("a,b,c\n") + assert_nil(csv.skip_lines) + end + + def test_regexp + csv = <<-CSV +1 +#2 + #3 +4 + CSV + assert_equal([ + ["1"], + ["4"], + ], + CSV.parse(csv, :skip_lines => /\A\s*#/)) + end + + def test_regexp_quoted + csv = <<-CSV +1 +#2 +"#3" +4 + CSV + assert_equal([ + ["1"], + ["#3"], + ["4"], + ], + CSV.parse(csv, :skip_lines => /\A\s*#/)) + end + + def test_string + csv = <<-CSV +1 +.2 +3. +4 + CSV + assert_equal([ + ["1"], + ["4"], + ], + CSV.parse(csv, :skip_lines => ".")) + end + + class RegexStub + end + + def test_not_matchable + regex_stub = RegexStub.new + csv = CSV.new("1\n", :skip_lines => regex_stub) + error = assert_raise(ArgumentError) do + csv.shift + end + assert_equal(":skip_lines has to respond to #match: #{regex_stub.inspect}", + error.message) + end + + class Matchable + def initialize(pattern) + @pattern = pattern + end + + def match(line) + @pattern.match(line) + end + end + + def test_matchable + csv = <<-CSV +1 +# 2 +3 +# 4 + CSV + assert_equal([ + ["1"], + ["3"], + ], + CSV.parse(csv, :skip_lines => Matchable.new(/\A#/))) + end + + def test_multibyte_data + # U+3042 HIRAGANA LETTER A + # U+3044 HIRAGANA LETTER I + # U+3046 HIRAGANA LETTER U + value = "\u3042\u3044\u3046" + with_chunk_size("5") do + assert_equal([[value], [value]], + CSV.parse("#{value}\n#{value}\n", + :skip_lines => /\A#/)) + end + end + + def test_empty_line_and_liberal_parsing + assert_equal([["a", "b"]], + CSV.parse("a,b\n", + :liberal_parsing => true, + :skip_lines => /^$/)) + end + + def test_crlf + assert_equal([["a", "b"]], + CSV.parse("a,b\r\n,\r\n", + :skip_lines => /^,+$/)) + end +end diff --git a/test/csv/parse/test_strip.rb b/test/csv/parse/test_strip.rb new file mode 100644 index 0000000000..c5e35209cc --- /dev/null +++ b/test/csv/parse/test_strip.rb @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +# frozen_string_literal: false + +require_relative "../helper" + +class TestCSVParseStrip < Test::Unit::TestCase + extend DifferentOFS + + def test_both + assert_equal(["a", "b"], + CSV.parse_line(%Q{ a , b }, strip: true)) + end + + def test_left + assert_equal(["a", "b"], + CSV.parse_line(%Q{ a, b}, strip: true)) + end + + def test_right + assert_equal(["a", "b"], + CSV.parse_line(%Q{a ,b }, strip: true)) + end + + def test_middle + assert_equal(["a b"], + CSV.parse_line(%Q{a b}, strip: true)) + end + + def test_quoted + assert_equal([" a ", " b "], + CSV.parse_line(%Q{" a "," b "}, strip: true)) + end + + def test_liberal_parsing + assert_equal([" a ", "b", " c ", " d "], + CSV.parse_line(%Q{" a ", b , " c "," d " }, + strip: true, + liberal_parsing: true)) + end + + def test_string + assert_equal(["a", " b"], + CSV.parse_line(%Q{ a , " b" }, + strip: " ")) + end + + def test_no_quote + assert_equal([" a ", " b "], + CSV.parse_line(%Q{" a ", b }, + strip: %Q{"}, + quote_char: nil)) + end + + def test_do_not_strip_cr + assert_equal([ + ["a", "b "], + ["a", "b "], + ], + CSV.parse(%Q{"a" ,"b " \r} + + %Q{"a" ,"b " \r}, + strip: true)) + end + + def test_do_not_strip_lf + assert_equal([ + ["a", "b "], + ["a", "b "], + ], + CSV.parse(%Q{"a" ,"b " \n} + + %Q{"a" ,"b " \n}, + strip: true)) + end + + def test_do_not_strip_crlf + assert_equal([ + ["a", "b "], + ["a", "b "], + ], + CSV.parse(%Q{"a" ,"b " \r\n} + + %Q{"a" ,"b " \r\n}, + strip: true)) + end + + def test_col_sep_incompatible_true + message = "The provided strip (true) and " \ + "col_sep (\\t) options are incompatible." + assert_raise_with_message(ArgumentError, message) do + CSV.parse_line(%Q{"a"\t"b"\n}, + col_sep: "\t", + strip: true) + end + end + + def test_col_sep_incompatible_string + message = "The provided strip (\\t) and " \ + "col_sep (\\t) options are incompatible." + assert_raise_with_message(ArgumentError, message) do + CSV.parse_line(%Q{"a"\t"b"\n}, + col_sep: "\t", + strip: "\t") + end + end + + def test_col_sep_compatible_string + assert_equal( + ["a", "b"], + CSV.parse_line(%Q{\va\tb\v\n}, + col_sep: "\t", + strip: "\v") + ) + end +end diff --git a/test/csv/parse/test_unconverted_fields.rb b/test/csv/parse/test_unconverted_fields.rb new file mode 100644 index 0000000000..437124ebd3 --- /dev/null +++ b/test/csv/parse/test_unconverted_fields.rb @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +# frozen_string_literal: false + +require_relative "../helper" + +class TestCSVParseUnconvertedFields < Test::Unit::TestCase + extend DifferentOFS + + def setup + super + @custom = lambda {|field| /\A:(\S.*?)\s*\Z/ =~ field ? $1.to_sym : field} + + @headers = ["first", "second", "third"] + @data = <<-CSV +first,second,third +1,2,3 + CSV + end + + + def test_custom + row = CSV.parse_line("Numbers,:integer,1,:float,3.015", + converters: [:numeric, @custom], + unconverted_fields: true) + assert_equal([ + ["Numbers", :integer, 1, :float, 3.015], + ["Numbers", ":integer", "1", ":float", "3.015"], + ], + [ + row, + row.unconverted_fields, + ]) + end + + def test_no_fields + row = CSV.parse_line("\n", + converters: [:numeric, @custom], + unconverted_fields: true) + assert_equal([ + [], + [], + ], + [ + row, + row.unconverted_fields, + ]) + end + + def test_parsed_header + row = CSV.parse_line(@data, + converters: :numeric, + unconverted_fields: true, + headers: :first_row) + assert_equal([ + CSV::Row.new(@headers, + [1, 2, 3]), + ["1", "2", "3"], + ], + [ + row, + row.unconverted_fields, + ]) + end + + def test_return_headers + row = CSV.parse_line(@data, + converters: :numeric, + unconverted_fields: true, + headers: :first_row, + return_headers: true) + assert_equal([ + CSV::Row.new(@headers, + @headers), + @headers, + ], + [ + row, + row.unconverted_fields, + ]) + end + + def test_header_converters + row = CSV.parse_line(@data, + converters: :numeric, + unconverted_fields: true, + headers: :first_row, + return_headers: true, + header_converters: :symbol) + assert_equal([ + CSV::Row.new(@headers.collect(&:to_sym), + @headers), + @headers, + ], + [ + row, + row.unconverted_fields, + ]) + end + + def test_specified_headers + row = CSV.parse_line("\n", + converters: :numeric, + unconverted_fields: true, + headers: %w{my new headers}, + return_headers: true, + header_converters: :symbol) + assert_equal([ + CSV::Row.new([:my, :new, :headers], + ["my", "new", "headers"]), + [], + ], + [ + row, + row.unconverted_fields, + ]) + end +end diff --git a/test/csv/test_csv_writing.rb b/test/csv/test_csv_writing.rb deleted file mode 100755 index 704c1d7ddc..0000000000 --- a/test/csv/test_csv_writing.rb +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env ruby -w -# encoding: UTF-8 - -# tc_csv_writing.rb -# -# Created by James Edward Gray II on 2005-10-31. -# Copyright 2005 James Edward Gray II. You can redistribute or modify this code -# under the terms of Ruby's license. - -require_relative "base" - -class TestCSV::Writing < TestCSV - extend DifferentOFS - - def test_writing - [ ["\t", ["\t"]], - ["foo,\"\"\"\"\"\",baz", ["foo", "\"\"", "baz"]], - ["foo,\"\"\"bar\"\"\",baz", ["foo", "\"bar\"", "baz"]], - ["\"\"\"\n\",\"\"\"\n\"", ["\"\n", "\"\n"]], - ["foo,\"\r\n\",baz", ["foo", "\r\n", "baz"]], - ["\"\"", [""]], - ["foo,\"\"\"\",baz", ["foo", "\"", "baz"]], - ["foo,\"\r.\n\",baz", ["foo", "\r.\n", "baz"]], - ["foo,\"\r\",baz", ["foo", "\r", "baz"]], - ["foo,\"\",baz", ["foo", "", "baz"]], - ["\",\"", [","]], - ["foo", ["foo"]], - [",,", [nil, nil, nil]], - [",", [nil, nil]], - ["foo,\"\n\",baz", ["foo", "\n", "baz"]], - ["foo,,baz", ["foo", nil, "baz"]], - ["\"\"\"\r\",\"\"\"\r\"", ["\"\r", "\"\r"]], - ["\",\",\",\"", [",", ","]], - ["foo,bar,", ["foo", "bar", nil]], - [",foo,bar", [nil, "foo", "bar"]], - ["foo,bar", ["foo", "bar"]], - [";", [";"]], - ["\t,\t", ["\t", "\t"]], - ["foo,\"\r\n\r\",baz", ["foo", "\r\n\r", "baz"]], - ["foo,\"\r\n\n\",baz", ["foo", "\r\n\n", "baz"]], - ["foo,\"foo,bar\",baz", ["foo", "foo,bar", "baz"]], - [";,;", [";", ";"]], - ["foo,\"\"\"\"\"\",baz", ["foo", "\"\"", "baz"]], - ["foo,\"\"\"bar\"\"\",baz", ["foo", "\"bar\"", "baz"]], - ["foo,\"\r\n\",baz", ["foo", "\r\n", "baz"]], - ["\"\"", [""]], - ["foo,\"\"\"\",baz", ["foo", "\"", "baz"]], - ["foo,\"\r.\n\",baz", ["foo", "\r.\n", "baz"]], - ["foo,\"\r\",baz", ["foo", "\r", "baz"]], - ["foo,\"\",baz", ["foo", "", "baz"]], - ["foo", ["foo"]], - [",,", [nil, nil, nil]], - [",", [nil, nil]], - ["foo,\"\n\",baz", ["foo", "\n", "baz"]], - ["foo,,baz", ["foo", nil, "baz"]], - ["foo,bar", ["foo", "bar"]], - ["foo,\"\r\n\n\",baz", ["foo", "\r\n\n", "baz"]], - ["foo,\"foo,bar\",baz", ["foo", "foo,bar", "baz"]], - [%Q{a,b}, ["a", "b"]], - [%Q{a,"""b"""}, ["a", "\"b\""]], - [%Q{a,"""b"}, ["a", "\"b"]], - [%Q{a,"b"""}, ["a", "b\""]], - [%Q{a,"\nb"""}, ["a", "\nb\""]], - [%Q{a,"""\nb"}, ["a", "\"\nb"]], - [%Q{a,"""\nb\n"""}, ["a", "\"\nb\n\""]], - [%Q{a,"""\nb\n""",}, ["a", "\"\nb\n\"", nil]], - [%Q{a,,,}, ["a", nil, nil, nil]], - [%Q{,}, [nil, nil]], - [%Q{"",""}, ["", ""]], - [%Q{""""}, ["\""]], - [%Q{"""",""}, ["\"",""]], - [%Q{,""}, [nil,""]], - [%Q{,"\r"}, [nil,"\r"]], - [%Q{"\r\n,"}, ["\r\n,"]], - [%Q{"\r\n,",}, ["\r\n,", nil]] ].each do |test_case| - assert_equal(test_case.first + $/, CSV.generate_line(test_case.last)) - end - end - - def test_col_sep - assert_equal( "a;b;;c\n", CSV.generate_line( ["a", "b", nil, "c"], - col_sep: ";" ) ) - assert_equal( "a\tb\t\tc\n", CSV.generate_line( ["a", "b", nil, "c"], - col_sep: "\t" ) ) - end - - def test_row_sep - assert_equal( "a,b,,c\r\n", CSV.generate_line( ["a", "b", nil, "c"], - row_sep: "\r\n" ) ) - end - - def test_force_quotes - assert_equal( %Q{"1","b","","already ""quoted"""\n}, - CSV.generate_line( [1, "b", nil, %Q{already "quoted"}], - force_quotes: true ) ) - end -end diff --git a/test/csv/test_data_converters.rb b/test/csv/test_data_converters.rb index 89b6dd1dfd..c20a5d1f4b 100755..100644 --- a/test/csv/test_data_converters.rb +++ b/test/csv/test_data_converters.rb @@ -1,24 +1,13 @@ -#!/usr/bin/env ruby -w -# encoding: UTF-8 +# -*- coding: utf-8 -*- +# frozen_string_literal: false -# tc_data_converters.rb -# -# Created by James Edward Gray II on 2005-10-31. -# Copyright 2005 James Edward Gray II. You can redistribute or modify this code -# under the terms of Ruby's license. +require_relative "helper" -require_relative "base" - -class TestCSV::DataConverters < TestCSV +class TestCSVDataConverters < Test::Unit::TestCase extend DifferentOFS def setup super - @data = "Numbers,:integer,1,:float,3.015" - @parser = CSV.new(@data) - - @custom = lambda { |field| field =~ /\A:(\S.*?)\s*\Z/ ? $1.to_sym : field } - @win_safe_time_str = Time.now.strftime("%a %b %d %H:%M:%S %Y") end @@ -66,198 +55,136 @@ class TestCSV::DataConverters < TestCSV assert_instance_of(String, CSV::Converters[:date_time]["junk"]) end - def test_convert_with_builtin_integer - # setup parser... - assert_respond_to(@parser, :convert) - assert_nothing_raised(Exception) { @parser.convert(:integer) } - - # and use - assert_equal(["Numbers", ":integer", 1, ":float", "3.015"], @parser.shift) + def test_builtin_date_time_converter_iso8601_date + iso8601_string = "2018-01-14" + datetime = DateTime.new(2018, 1, 14) + assert_equal(datetime, + CSV::Converters[:date_time][iso8601_string]) end - def test_convert_with_builtin_float - # setup parser... - assert_respond_to(@parser, :convert) - assert_nothing_raised(Exception) { @parser.convert(:float) } - - # and use - assert_equal(["Numbers", ":integer", 1.0, ":float", 3.015], @parser.shift) + def test_builtin_date_time_converter_iso8601_minute + iso8601_string = "2018-01-14T22:25" + datetime = DateTime.new(2018, 1, 14, 22, 25) + assert_equal(datetime, + CSV::Converters[:date_time][iso8601_string]) end - def test_convert_order_float_integer - # floats first, then integers... - assert_nothing_raised(Exception) do - @parser.convert(:float) - @parser.convert(:integer) - end - - # gets us nothing but floats - assert_equal( [String, String, Float, String, Float], - @parser.shift.map { |field| field.class } ) + def test_builtin_date_time_converter_iso8601_second + iso8601_string = "2018-01-14T22:25:19" + datetime = DateTime.new(2018, 1, 14, 22, 25, 19) + assert_equal(datetime, + CSV::Converters[:date_time][iso8601_string]) end - def test_convert_order_integer_float - # integers have precendance... - assert_nothing_raised(Exception) do - @parser.convert(:integer) - @parser.convert(:float) - end - - # gives us proper number conversion - assert_equal( [String, String, Fixnum, String, Float], - @parser.shift.map { |field| field.class } ) + def test_builtin_date_time_converter_iso8601_under_second + iso8601_string = "2018-01-14T22:25:19.1" + datetime = DateTime.new(2018, 1, 14, 22, 25, 19.1) + assert_equal(datetime, + CSV::Converters[:date_time][iso8601_string]) end - def test_builtin_numeric_combo_converter - # setup parser... - assert_nothing_raised(Exception) { @parser.convert(:numeric) } - - # and use - assert_equal( [String, String, Fixnum, String, Float], - @parser.shift.map { |field| field.class } ) + def test_builtin_date_time_converter_iso8601_under_second_offset + iso8601_string = "2018-01-14T22:25:19.1+09:00" + datetime = DateTime.new(2018, 1, 14, 22, 25, 19.1, "+9") + assert_equal(datetime, + CSV::Converters[:date_time][iso8601_string]) end - def test_builtin_all_nested_combo_converter - # setup parser... - @data << ",#{@win_safe_time_str}" # add a DateTime field - @parser = CSV.new(@data) # reset parser - assert_nothing_raised(Exception) { @parser.convert(:all) } - - # and use - assert_equal( [String, String, Fixnum, String, Float, DateTime], - @parser.shift.map { |field| field.class } ) + def test_builtin_date_time_converter_iso8601_offset + iso8601_string = "2018-01-14T22:25:19+09:00" + datetime = DateTime.new(2018, 1, 14, 22, 25, 19, "+9") + assert_equal(datetime, + CSV::Converters[:date_time][iso8601_string]) end - def test_convert_with_custom_code - # define custom converter... - assert_nothing_raised(Exception) do - @parser.convert { |field| field =~ /\A:(\S.*?)\s*\Z/ ? $1.to_sym : field } - end - - # and use - assert_equal(["Numbers", :integer, "1", :float, "3.015"], @parser.shift) + def test_builtin_date_time_converter_iso8601_utc + iso8601_string = "2018-01-14T22:25:19Z" + datetime = DateTime.new(2018, 1, 14, 22, 25, 19) + assert_equal(datetime, + CSV::Converters[:date_time][iso8601_string]) end - def test_convert_with_custom_code_mix - # mix built-in and custom... - assert_nothing_raised(Exception) { @parser.convert(:numeric) } - assert_nothing_raised(Exception) { @parser.convert(&@custom) } - - # and use - assert_equal(["Numbers", :integer, 1, :float, 3.015], @parser.shift) + def test_builtin_date_time_converter_rfc3339_minute + rfc3339_string = "2018-01-14 22:25" + datetime = DateTime.new(2018, 1, 14, 22, 25) + assert_equal(datetime, + CSV::Converters[:date_time][rfc3339_string]) end - def test_convert_with_custom_code_using_field_info - # define custom converter that uses field information... - assert_nothing_raised(Exception) do - @parser.convert do |field, info| - assert_equal(1, info.line) - info.index == 4 ? Float(field).floor : field - end - end - - # and use - assert_equal(["Numbers", ":integer", "1", ":float", 3], @parser.shift) + def test_builtin_date_time_converter_rfc3339_second + rfc3339_string = "2018-01-14 22:25:19" + datetime = DateTime.new(2018, 1, 14, 22, 25, 19) + assert_equal(datetime, + CSV::Converters[:date_time][rfc3339_string]) end - def test_convert_with_custom_code_using_field_info_header - @parser = CSV.new(@data, headers: %w{one two three four five}) - - # define custom converter that uses field header information... - assert_nothing_raised(Exception) do - @parser.convert do |field, info| - info.header == "three" ? Integer(field) * 100 : field - end - end + def test_builtin_date_time_converter_rfc3339_under_second + rfc3339_string = "2018-01-14 22:25:19.1" + datetime = DateTime.new(2018, 1, 14, 22, 25, 19.1) + assert_equal(datetime, + CSV::Converters[:date_time][rfc3339_string]) + end - # and use - assert_equal( ["Numbers", ":integer", 100, ":float", "3.015"], - @parser.shift.fields ) + def test_builtin_date_time_converter_rfc3339_under_second_offset + rfc3339_string = "2018-01-14 22:25:19.1+09:00" + datetime = DateTime.new(2018, 1, 14, 22, 25, 19.1, "+9") + assert_equal(datetime, + CSV::Converters[:date_time][rfc3339_string]) end - def test_shortcut_interface - assert_equal( ["Numbers", ":integer", 1, ":float", 3.015], - CSV.parse_line(@data, converters: :numeric) ) + def test_builtin_date_time_converter_rfc3339_offset + rfc3339_string = "2018-01-14 22:25:19+09:00" + datetime = DateTime.new(2018, 1, 14, 22, 25, 19, "+9") + assert_equal(datetime, + CSV::Converters[:date_time][rfc3339_string]) + end - assert_equal( ["Numbers", ":integer", 1, ":float", 3.015], - CSV.parse_line(@data, converters: [:integer, :float]) ) + def test_builtin_date_time_converter_rfc3339_utc + rfc3339_string = "2018-01-14 22:25:19Z" + datetime = DateTime.new(2018, 1, 14, 22, 25, 19) + assert_equal(datetime, + CSV::Converters[:date_time][rfc3339_string]) + end - assert_equal( ["Numbers", :integer, 1, :float, 3.015], - CSV.parse_line(@data, converters: [:numeric, @custom]) ) + def test_builtin_date_time_converter_rfc3339_tab_minute + rfc3339_string = "2018-01-14\t22:25" + datetime = DateTime.new(2018, 1, 14, 22, 25) + assert_equal(datetime, + CSV::Converters[:date_time][rfc3339_string]) end - def test_unconverted_fields - [ [ @data, - ["Numbers", :integer, 1, :float, 3.015], - %w{Numbers :integer 1 :float 3.015} ], - ["\n", Array.new, Array.new] ].each do |test, fields, unconverted| - row = nil - assert_nothing_raised(Exception) do - row = CSV.parse_line( test, - converters: [:numeric, @custom], - unconverted_fields: true ) - end - assert_not_nil(row) - assert_equal(fields, row) - assert_respond_to(row, :unconverted_fields) - assert_equal(unconverted, row.unconverted_fields) - end + def test_builtin_date_time_converter_rfc3339_tab_second + rfc3339_string = "2018-01-14\t22:25:19" + datetime = DateTime.new(2018, 1, 14, 22, 25, 19) + assert_equal(datetime, + CSV::Converters[:date_time][rfc3339_string]) + end - data = <<-END_CSV.gsub(/^\s+/, "") - first,second,third - 1,2,3 - END_CSV - row = nil - assert_nothing_raised(Exception) do - row = CSV.parse_line( data, - converters: :numeric, - unconverted_fields: true, - headers: :first_row ) - end - assert_not_nil(row) - assert_equal([["first", 1], ["second", 2], ["third", 3]], row.to_a) - assert_respond_to(row, :unconverted_fields) - assert_equal(%w{1 2 3}, row.unconverted_fields) + def test_builtin_date_time_converter_rfc3339_tab_under_second + rfc3339_string = "2018-01-14\t22:25:19.1" + datetime = DateTime.new(2018, 1, 14, 22, 25, 19.1) + assert_equal(datetime, + CSV::Converters[:date_time][rfc3339_string]) + end - assert_nothing_raised(Exception) do - row = CSV.parse_line( data, - converters: :numeric, - unconverted_fields: true, - headers: :first_row, - return_headers: true ) - end - assert_not_nil(row) - assert_equal( [%w{first first}, %w{second second}, %w{third third}], - row.to_a ) - assert_respond_to(row, :unconverted_fields) - assert_equal(%w{first second third}, row.unconverted_fields) + def test_builtin_date_time_converter_rfc3339_tab_under_second_offset + rfc3339_string = "2018-01-14\t22:25:19.1+09:00" + datetime = DateTime.new(2018, 1, 14, 22, 25, 19.1, "+9") + assert_equal(datetime, + CSV::Converters[:date_time][rfc3339_string]) + end - assert_nothing_raised(Exception) do - row = CSV.parse_line( data, - converters: :numeric, - unconverted_fields: true, - headers: :first_row, - return_headers: true, - header_converters: :symbol ) - end - assert_not_nil(row) - assert_equal( [[:first, "first"], [:second, "second"], [:third, "third"]], - row.to_a ) - assert_respond_to(row, :unconverted_fields) - assert_equal(%w{first second third}, row.unconverted_fields) + def test_builtin_date_time_converter_rfc3339_tab_offset + rfc3339_string = "2018-01-14\t22:25:19+09:00" + datetime = DateTime.new(2018, 1, 14, 22, 25, 19, "+9") + assert_equal(datetime, + CSV::Converters[:date_time][rfc3339_string]) + end - assert_nothing_raised(Exception) do - row = CSV.parse_line( data, - converters: :numeric, - unconverted_fields: true, - headers: %w{my new headers}, - return_headers: true, - header_converters: :symbol ) - end - assert_not_nil(row) - assert_equal( [[:my, "my"], [:new, "new"], [:headers, "headers"]], - row.to_a ) - assert_respond_to(row, :unconverted_fields) - assert_equal(Array.new, row.unconverted_fields) + def test_builtin_date_time_converter_rfc3339_tab_utc + rfc3339_string = "2018-01-14\t22:25:19Z" + datetime = DateTime.new(2018, 1, 14, 22, 25, 19) + assert_equal(datetime, + CSV::Converters[:date_time][rfc3339_string]) end end diff --git a/test/csv/test_encodings.rb b/test/csv/test_encodings.rb index dc45692ce4..f08d551f69 100755..100644 --- a/test/csv/test_encodings.rb +++ b/test/csv/test_encodings.rb @@ -1,16 +1,11 @@ -#!/usr/bin/env ruby -w -# encoding: UTF-8 +# -*- coding: utf-8 -*- +# frozen_string_literal: false -# tc_encodings.rb -# -# Created by James Edward Gray II on 2008-09-13. -# Copyright 2008 James Edward Gray II. You can redistribute or modify this code -# under the terms of Ruby's license. +require_relative "helper" -require_relative "base" - -class TestCSV::Encodings < TestCSV +class TestCSVEncodings < Test::Unit::TestCase extend DifferentOFS + include Helper def setup super @@ -247,6 +242,18 @@ class TestCSV::Encodings < TestCSV assert_equal("UTF-8", data.to_csv.encoding.name) end + def test_encoding_is_not_upgraded_for_non_ascii_content_during_writing_as_needed + data = ["\u00c0".encode("ISO-8859-1"), "\u3042"] + assert_equal([ + "ISO-8859-1", + "UTF-8", + ], + data.collect {|field| field.encoding.name}) + assert_raise(Encoding::CompatibilityError) do + data.to_csv + end + end + def test_explicit_encoding bug9766 = '[ruby-core:62113] [Bug #9766]' s = CSV.generate(encoding: "Windows-31J") do |csv| @@ -255,26 +262,85 @@ class TestCSV::Encodings < TestCSV assert_equal(["foo,\u3042\n".encode(Encoding::Windows_31J), Encoding::Windows_31J], [s, s.encoding], bug9766) end + def test_encoding_with_default_internal + with_default_internal(Encoding::UTF_8) do + s = CSV.generate(String.new(encoding: Encoding::Big5), encoding: Encoding::Big5) do |csv| + csv << ["漢字"] + end + assert_equal(["漢字\n".encode(Encoding::Big5), Encoding::Big5], [s, s.encoding]) + end + end + + def test_row_separator_detection_with_invalid_encoding + csv = CSV.new("invalid,\xF8\r\nvalid,x\r\n".force_encoding("UTF-8"), + encoding: "UTF-8") + assert_equal("\r\n", csv.row_sep) + end + + def test_invalid_encoding_row_error + csv = CSV.new("valid,x\rinvalid,\xF8\r".force_encoding("UTF-8"), + encoding: "UTF-8", row_sep: "\r") + error = assert_raise(CSV::MalformedCSVError) do + csv.shift + csv.shift + end + assert_equal("Invalid byte sequence in UTF-8 in line 2.", + error.message) + end + + def test_string_input_transcode + # U+3042 HIRAGANA LETTER A + # U+3044 HIRAGANA LETTER I + # U+3046 HIRAGANA LETTER U + value = "\u3042\u3044\u3046" + csv = CSV.new(value, encoding: "UTF-8:EUC-JP") + assert_equal([[value.encode("EUC-JP")]], + csv.read) + end + + def test_string_input_set_encoding_string + # U+3042 HIRAGANA LETTER A + # U+3044 HIRAGANA LETTER I + # U+3046 HIRAGANA LETTER U + value = "\u3042\u3044\u3046".encode("EUC-JP") + csv = CSV.new(value.dup.force_encoding("UTF-8"), encoding: "EUC-JP") + assert_equal([[value.encode("EUC-JP")]], + csv.read) + end + + def test_string_input_set_encoding_encoding + # U+3042 HIRAGANA LETTER A + # U+3044 HIRAGANA LETTER I + # U+3046 HIRAGANA LETTER U + value = "\u3042\u3044\u3046".encode("EUC-JP") + csv = CSV.new(value.dup.force_encoding("UTF-8"), + encoding: Encoding.find("EUC-JP")) + assert_equal([[value.encode("EUC-JP")]], + csv.read) + end + private - def assert_parses(fields, encoding, options = { }) + def assert_parses(fields, encoding, **options) encoding = Encoding.find(encoding) unless encoding.is_a? Encoding orig_fields = fields - fields = encode_ary(fields, encoding) - data = ary_to_data(fields, options) - parsed = CSV.parse(data, options) + fields = encode_ary(fields, encoding) + data = ary_to_data(fields, **options) + parsed = CSV.parse(data, **options) assert_equal(fields, parsed) parsed.flatten.each_with_index do |field, i| assert_equal(encoding, field.encoding, "Field[#{i + 1}] was transcoded.") end File.open(@temp_csv_path, "wb") {|f| f.print(data)} - CSV.open(@temp_csv_path, "rb:#{encoding}", options) do |csv| + CSV.open(@temp_csv_path, "rb:#{encoding}", **options) do |csv| csv.each_with_index do |row, i| assert_equal(fields[i], row) end end begin - CSV.open(@temp_csv_path, "rb:#{encoding}:#{__ENCODING__}", options) do |csv| + CSV.open(@temp_csv_path, + "rb:#{encoding}:#{__ENCODING__}", + **options) do |csv| csv.each_with_index do |row, i| assert_equal(orig_fields[i], row) end @@ -282,7 +348,7 @@ class TestCSV::Encodings < TestCSV rescue Encoding::ConverterNotFoundError end options[:encoding] = encoding.name - CSV.open(@temp_csv_path, options) do |csv| + CSV.open(@temp_csv_path, **options) do |csv| csv.each_with_index do |row, i| assert_equal(fields[i], row) end @@ -291,7 +357,7 @@ class TestCSV::Encodings < TestCSV options[:external_encoding] = encoding.name options[:internal_encoding] = __ENCODING__.name begin - CSV.open(@temp_csv_path, options) do |csv| + CSV.open(@temp_csv_path, **options) do |csv| csv.each_with_index do |row, i| assert_equal(orig_fields[i], row) end @@ -304,7 +370,7 @@ class TestCSV::Encodings < TestCSV ary.map { |row| row.map { |field| field.encode(encoding) } } end - def ary_to_data(ary, options = { }) + def ary_to_data(ary, **options) encoding = ary.flatten.first.encoding quote_char = (options[:quote_char] || '"').encode(encoding) col_sep = (options[:col_sep] || ",").encode(encoding) @@ -316,9 +382,9 @@ class TestCSV::Encodings < TestCSV }.join('').encode(encoding) end - def encode_for_tests(data, options = { }) - yield ary_to_data(encode_ary(data, "UTF-8"), options) - yield ary_to_data(encode_ary(data, "UTF-16BE"), options) + def encode_for_tests(data, **options) + yield ary_to_data(encode_ary(data, "UTF-8"), **options) + yield ary_to_data(encode_ary(data, "UTF-16BE"), **options) end def each_encoding diff --git a/test/csv/test_features.rb b/test/csv/test_features.rb index e31465710b..d6eb2dc13b 100755..100644 --- a/test/csv/test_features.rb +++ b/test/csv/test_features.rb @@ -1,21 +1,15 @@ -#!/usr/bin/env ruby -w -# encoding: UTF-8 - -# tc_features.rb -# -# Created by James Edward Gray II on 2005-10-31. -# Copyright 2005 James Edward Gray II. You can redistribute or modify this code -# under the terms of Ruby's license. +# -*- coding: utf-8 -*- +# frozen_string_literal: false begin require "zlib" rescue LoadError end -require_relative "base" +require_relative "helper" require "tempfile" -class TestCSV::Features < TestCSV +class TestCSVFeatures < Test::Unit::TestCase extend DifferentOFS TEST_CASES = [ [%Q{a,b}, ["a", "b"]], @@ -38,12 +32,12 @@ class TestCSV::Features < TestCSV def setup super - @sample_data = <<-END_DATA.gsub(/^ +/, "") - line,1,abc - line,2,"def\nghi" + @sample_data = <<-CSV +line,1,abc +line,2,"def\nghi" - line,4,jkl - END_DATA +line,4,jkl + CSV @csv = CSV.new(@sample_data) end @@ -58,27 +52,52 @@ class TestCSV::Features < TestCSV assert_equal([",,,", nil], CSV.parse_line(",,,;", col_sep: ";")) end + def test_col_sep_nil + assert_raise_with_message(ArgumentError, + ":col_sep must be 1 or more characters: nil") do + CSV.parse(@sample_data, col_sep: nil) + end + end + + def test_col_sep_empty + assert_raise_with_message(ArgumentError, + ":col_sep must be 1 or more characters: \"\"") do + CSV.parse(@sample_data, col_sep: "") + end + end + def test_row_sep - assert_raise(CSV::MalformedCSVError) do - CSV.parse_line("1,2,3\n,4,5\r\n", row_sep: "\r\n") + error = assert_raise(CSV::MalformedCSVError) do + CSV.parse_line("1,2,3\n,4,5\r\n", row_sep: "\r\n") end + assert_equal("Unquoted fields do not allow new line <\"\\n\"> in line 1.", + error.message) assert_equal( ["1", "2", "3\n", "4", "5"], CSV.parse_line(%Q{1,2,"3\n",4,5\r\n}, row_sep: "\r\n")) end def test_quote_char TEST_CASES.each do |test_case| - assert_equal( test_case.last.map { |t| t.tr('"', "'") unless t.nil? }, - CSV.parse_line( test_case.first.tr('"', "'"), - quote_char: "'" ) ) + assert_equal(test_case.last.map {|t| t.tr('"', "'") unless t.nil?}, + CSV.parse_line(test_case.first.tr('"', "'"), + quote_char: "'" )) + end + end + + def test_quote_char_special_regexp_char + TEST_CASES.each do |test_case| + assert_equal(test_case.last.map {|t| t.tr('"', "|") unless t.nil?}, + CSV.parse_line(test_case.first.tr('"', "|"), + quote_char: "|")) end end - def test_bug_8405 + def test_quote_char_special_regexp_char_liberal_parsing TEST_CASES.each do |test_case| - assert_equal( test_case.last.map { |t| t.tr('"', "|") unless t.nil? }, - CSV.parse_line( test_case.first.tr('"', "|"), - quote_char: "|" ) ) + assert_equal(test_case.last.map {|t| t.tr('"', "|") unless t.nil?}, + CSV.parse_line(test_case.first.tr('"', "|"), + quote_char: "|", + liberal_parsing: true)) end end @@ -103,6 +122,20 @@ class TestCSV::Features < TestCSV assert_equal($/, CSV.new(STDERR).row_sep) end + def test_line + lines = [ + %Q(\u{3000}abc,def\n), + %Q(\u{3000}abc,"d\nef"\n), + %Q(\u{3000}abc,"d\r\nef"\n), + %Q(\u{3000}abc,"d\ref") + ] + csv = CSV.new(lines.join('')) + lines.each do |line| + csv.shift + assert_equal(line, csv.line) + end + end + def test_lineno assert_equal(5, @sample_data.lines.to_a.size) @@ -123,9 +156,12 @@ class TestCSV::Features < TestCSV end def test_unknown_options - assert_raise_with_message(ArgumentError, /unknown/) { + assert_raise_with_message(ArgumentError, /unknown keyword/) { CSV.new(@sample_data, unknown: :error) } + assert_raise_with_message(ArgumentError, /unknown keyword/) { + CSV.new(@sample_data, universal_newline: true) + } end def test_skip_blanks @@ -186,29 +222,19 @@ class TestCSV::Features < TestCSV end # reported by Kev Jackson - def test_failing_to_escape_col_sep_bug_fix + def test_failing_to_escape_col_sep assert_nothing_raised(Exception) { CSV.new(String.new, col_sep: "|") } end # reported by Chris Roos - def test_failing_to_reset_headers_in_rewind_bug_fix + def test_failing_to_reset_headers_in_rewind csv = CSV.new("forename,surname", headers: true, return_headers: true) csv.each {|row| assert_predicate row, :header_row?} csv.rewind csv.each {|row| assert_predicate row, :header_row?} end - # reported by Dave Burt - def test_leading_empty_fields_with_multibyte_col_sep_bug_fix - data = <<-END_DATA.gsub(/^\s+/, "") - <=><=>A<=>B<=>C - 1<=>2<=>3 - END_DATA - parsed = CSV.parse(data, col_sep: "<=>") - assert_equal([[nil, nil, "A", "B", "C"], ["1", "2", "3"]], parsed) - end - - def test_gzip_reader_bug_fix + def test_gzip_reader zipped = nil assert_nothing_raised(NoMethodError) do zipped = CSV.new( @@ -222,7 +248,7 @@ class TestCSV::Features < TestCSV zipped.close end if defined?(Zlib::GzipReader) - def test_gzip_writer_bug_fix + def test_gzip_writer Tempfile.create(%w"temp .gz") {|tempfile| tempfile.close file = tempfile.path @@ -263,65 +289,71 @@ class TestCSV::Features < TestCSV end def test_inspect_shows_headers_when_available - CSV.new("one,two,three\n1,2,3\n", headers: true) do |csv| - assert_include(csv.inspect, "headers:true", "Header hint not shown.") - csv.shift # load headers - assert_match(/headers:\[[^\]]+\]/, csv.inspect) - end + csv = CSV.new("one,two,three\n1,2,3\n", headers: true) + assert_include(csv.inspect, "headers:true", "Header hint not shown.") + csv.shift # load headers + assert_match(/headers:\[[^\]]+\]/, csv.inspect) end def test_inspect_encoding_is_ascii_compatible - CSV.new("one,two,three\n1,2,3\n".encode("UTF-16BE")) do |csv| - assert_send([Encoding, :compatible?, - Encoding.find("US-ASCII"), csv.inspect.encoding], - "inspect() was not ASCII compatible.") - end + csv = CSV.new("one,two,three\n1,2,3\n".encode("UTF-16BE")) + assert_send([Encoding, :compatible?, + Encoding.find("US-ASCII"), csv.inspect.encoding], + "inspect() was not ASCII compatible.") end def test_version assert_not_nil(CSV::VERSION) assert_instance_of(String, CSV::VERSION) assert_predicate(CSV::VERSION, :frozen?) - assert_match(/\A\d\.\d\.\d\Z/, CSV::VERSION) + assert_match(/\A\d\.\d\.\d\z/, CSV::VERSION) end - def test_accepts_comment_skip_lines_option - assert_nothing_raised(ArgumentError) do - CSV.new(@sample_data, :skip_lines => /\A\s*#/) - end + def test_table_nil_equality + assert_nothing_raised(NoMethodError) { CSV.parse("test", headers: true) == nil } end - def test_accepts_comment_defaults_to_nil - c = CSV.new(@sample_data) - assert_nil(c.skip_lines) + # non-seekable input stream for testing https://github.com/ruby/csv/issues/44 + class DummyIO + extend Forwardable + def_delegators :@io, :gets, :read, :pos, :eof? # no seek or rewind! + def initialize(data) + @io = StringIO.new(data) + end end - class RegexStub + def test_line_separator_autodetection_for_non_seekable_input_lf + c = CSV.new(DummyIO.new("one,two,three\nfoo,bar,baz\n")) + assert_equal [["one", "two", "three"], ["foo", "bar", "baz"]], c.each.to_a end - def test_requires_skip_lines_to_call_match - regex_stub = RegexStub.new - assert_raise_with_message(ArgumentError, /skip_lines/) do - CSV.new(@sample_data, :skip_lines => regex_stub) - end + def test_line_separator_autodetection_for_non_seekable_input_cr + c = CSV.new(DummyIO.new("one,two,three\rfoo,bar,baz\r")) + assert_equal [["one", "two", "three"], ["foo", "bar", "baz"]], c.each.to_a end - def test_comment_rows_are_ignored - sample_data = "line,1,a\n#not,a,line\nline,2,b\n #also,no,line" - c = CSV.new sample_data, :skip_lines => /\A\s*#/ - assert_equal [["line", "1", "a"], ["line", "2", "b"]], c.each.to_a + def test_line_separator_autodetection_for_non_seekable_input_cr_lf + c = CSV.new(DummyIO.new("one,two,three\r\nfoo,bar,baz\r\n")) + assert_equal [["one", "two", "three"], ["foo", "bar", "baz"]], c.each.to_a end - def test_quoted_skip_line_markers_are_ignored - sample_data = "line,1,a\n\"#not\",a,line\nline,2,b" - c = CSV.new sample_data, :skip_lines => /\A\s*#/ - assert_equal [["line", "1", "a"], ["#not", "a", "line"], ["line", "2", "b"]], c.each.to_a + def test_line_separator_autodetection_for_non_seekable_input_1024_over_lf + table = (1..10).map { |row| (1..200).map { |col| "row#{row}col#{col}" }.to_a }.to_a + input = table.map { |line| line.join(",") }.join("\n") + c = CSV.new(DummyIO.new(input)) + assert_equal table, c.each.to_a end - def test_string_works_like_a_regexp - sample_data = "line,1,a\n#(not,a,line\nline,2,b\n also,#no,line" - c = CSV.new sample_data, :skip_lines => "#" - assert_equal [["line", "1", "a"], ["line", "2", "b"]], c.each.to_a + def test_line_separator_autodetection_for_non_seekable_input_1024_over_cr_lf + table = (1..10).map { |row| (1..200).map { |col| "row#{row}col#{col}" }.to_a }.to_a + input = table.map { |line| line.join(",") }.join("\r\n") + c = CSV.new(DummyIO.new(input)) + assert_equal table, c.each.to_a end + def test_line_separator_autodetection_for_non_seekable_input_many_cr_only + # input with lots of CRs (to make sure no bytes are lost due to look-ahead) + c = CSV.new(DummyIO.new("foo\r" + "\r" * 9999 + "bar\r")) + assert_equal [["foo"]] + [[]] * 9999 + [["bar"]], c.each.to_a + end end diff --git a/test/csv/test_interface.rb b/test/csv/test_interface.rb deleted file mode 100755 index d6bf470f6b..0000000000 --- a/test/csv/test_interface.rb +++ /dev/null @@ -1,368 +0,0 @@ -#!/usr/bin/env ruby -w -# encoding: UTF-8 - -# tc_interface.rb -# -# Created by James Edward Gray II on 2005-10-31. -# Copyright 2005 James Edward Gray II. You can redistribute or modify this code -# under the terms of Ruby's license. - -require_relative "base" -require "tempfile" - -class TestCSV::Interface < TestCSV - extend DifferentOFS - - def setup - super - @tempfile = Tempfile.new(%w"temp .csv") - @tempfile.close - @path = @tempfile.path - - File.open(@path, "wb") do |file| - file << "1\t2\t3\r\n" - file << "4\t5\r\n" - end - - @expected = [%w{1 2 3}, %w{4 5}] - end - - def teardown - @tempfile.close(true) - super - end - - ### Test Read Interface ### - - def test_foreach - CSV.foreach(@path, col_sep: "\t", row_sep: "\r\n") do |row| - assert_equal(@expected.shift, row) - end - end - - def test_foreach_enum - CSV.foreach(@path, col_sep: "\t", row_sep: "\r\n").zip(@expected) do |row, exp| - assert_equal(exp, row) - end - end - - def test_open_and_close - csv = CSV.open(@path, "r+", col_sep: "\t", row_sep: "\r\n") - assert_not_nil(csv) - assert_instance_of(CSV, csv) - assert_not_predicate(csv, :closed?) - csv.close - assert_predicate(csv, :closed?) - - ret = CSV.open(@path) do |new_csv| - csv = new_csv - assert_instance_of(CSV, new_csv) - "Return value." - end - assert_predicate(csv, :closed?) - assert_equal("Return value.", ret) - end - - def test_parse - data = File.binread(@path) - assert_equal( @expected, - CSV.parse(data, col_sep: "\t", row_sep: "\r\n") ) - - CSV.parse(data, col_sep: "\t", row_sep: "\r\n") do |row| - assert_equal(@expected.shift, row) - end - end - - def test_parse_line - row = CSV.parse_line("1;2;3", col_sep: ";") - assert_not_nil(row) - assert_instance_of(Array, row) - assert_equal(%w{1 2 3}, row) - - # shortcut interface - row = "1;2;3".parse_csv(col_sep: ";") - assert_not_nil(row) - assert_instance_of(Array, row) - assert_equal(%w{1 2 3}, row) - end - - def test_parse_line_with_empty_lines - assert_equal(nil, CSV.parse_line("")) # to signal eof - assert_equal(Array.new, CSV.parse_line("\n1,2,3")) - end - - def test_read_and_readlines - assert_equal( @expected, - CSV.read(@path, col_sep: "\t", row_sep: "\r\n") ) - assert_equal( @expected, - CSV.readlines(@path, col_sep: "\t", row_sep: "\r\n") ) - - - data = CSV.open(@path, col_sep: "\t", row_sep: "\r\n") do |csv| - csv.read - end - assert_equal(@expected, data) - data = CSV.open(@path, col_sep: "\t", row_sep: "\r\n") do |csv| - csv.readlines - end - assert_equal(@expected, data) - end - - def test_table - table = CSV.table(@path, col_sep: "\t", row_sep: "\r\n") - assert_instance_of(CSV::Table, table) - assert_equal([[:"1", :"2", :"3"], [4, 5, nil]], table.to_a) - end - - def test_shift # aliased as gets() and readline() - CSV.open(@path, "rb+", col_sep: "\t", row_sep: "\r\n") do |csv| - assert_equal(@expected.shift, csv.shift) - assert_equal(@expected.shift, csv.shift) - assert_equal(nil, csv.shift) - end - end - - def test_enumerators_are_supported - CSV.open(@path, col_sep: "\t", row_sep: "\r\n") do |csv| - enum = csv.each - assert_instance_of(Enumerator, enum) - assert_equal(@expected.shift, enum.next) - end - end - - def test_nil_is_not_acceptable - assert_raise_with_message ArgumentError, "Cannot parse nil as CSV" do - CSV.new(nil) - end - end - - ### Test Write Interface ### - - def test_generate - str = CSV.generate do |csv| # default empty String - assert_instance_of(CSV, csv) - assert_equal(csv, csv << [1, 2, 3]) - assert_equal(csv, csv << [4, nil, 5]) - end - assert_not_nil(str) - assert_instance_of(String, str) - assert_equal("1,2,3\n4,,5\n", str) - - CSV.generate(str) do |csv| # appending to a String - assert_equal(csv, csv << ["last", %Q{"row"}]) - end - assert_equal(%Q{1,2,3\n4,,5\nlast,"""row"""\n}, str) - end - - def test_generate_line - line = CSV.generate_line(%w{1 2 3}, col_sep: ";") - assert_not_nil(line) - assert_instance_of(String, line) - assert_equal("1;2;3\n", line) - - # shortcut interface - line = %w{1 2 3}.to_csv(col_sep: ";") - assert_not_nil(line) - assert_instance_of(String, line) - assert_equal("1;2;3\n", line) - end - - def test_write_header_detection - File.unlink(@path) - - headers = %w{a b c} - CSV.open(@path, "w", headers: true) do |csv| - csv << headers - csv << %w{1 2 3} - assert_equal(headers, csv.instance_variable_get(:@headers)) - end - end - - def test_write_lineno - File.unlink(@path) - - CSV.open(@path, "w") do |csv| - lines = 20 - lines.times { csv << %w{a b c} } - assert_equal(lines, csv.lineno) - end - end - - def test_write_hash - File.unlink(@path) - - lines = [{a: 1, b: 2, c: 3}, {a: 4, b: 5, c: 6}] - CSV.open( @path, "wb", headers: true, - header_converters: :symbol ) do |csv| - csv << lines.first.keys - lines.each { |line| csv << line } - end - CSV.open( @path, "rb", headers: true, - converters: :all, - header_converters: :symbol ) do |csv| - csv.each { |line| assert_equal(lines.shift, line.to_hash) } - end - end - - def test_write_hash_with_string_keys - File.unlink(@path) - - lines = [{a: 1, b: 2, c: 3}, {a: 4, b: 5, c: 6}] - CSV.open( @path, "wb", headers: true ) do |csv| - csv << lines.first.keys - lines.each { |line| csv << line } - end - CSV.open( @path, "rb", headers: true ) do |csv| - csv.each do |line| - csv.headers.each_with_index do |header, h| - keys = line.to_hash.keys - assert_instance_of(String, keys[h]) - assert_same(header, keys[h]) - end - end - end - end - - def test_write_hash_with_headers_array - File.unlink(@path) - - lines = [{a: 1, b: 2, c: 3}, {a: 4, b: 5, c: 6}] - CSV.open(@path, "wb", headers: [:b, :a, :c]) do |csv| - lines.each { |line| csv << line } - end - - # test writing fields in the correct order - File.open(@path, "rb") do |f| - assert_equal("2,1,3", f.gets.strip) - assert_equal("5,4,6", f.gets.strip) - end - - # test reading CSV with headers - CSV.open( @path, "rb", headers: [:b, :a, :c], - converters: :all ) do |csv| - csv.each { |line| assert_equal(lines.shift, line.to_hash) } - end - end - - def test_write_hash_with_headers_string - File.unlink(@path) - - lines = [{"a" => 1, "b" => 2, "c" => 3}, {"a" => 4, "b" => 5, "c" => 6}] - CSV.open(@path, "wb", headers: "b|a|c", col_sep: "|") do |csv| - lines.each { |line| csv << line } - end - - # test writing fields in the correct order - File.open(@path, "rb") do |f| - assert_equal("2|1|3", f.gets.strip) - assert_equal("5|4|6", f.gets.strip) - end - - # test reading CSV with headers - CSV.open( @path, "rb", headers: "b|a|c", - col_sep: "|", - converters: :all ) do |csv| - csv.each { |line| assert_equal(lines.shift, line.to_hash) } - end - end - - def test_write_headers - File.unlink(@path) - - lines = [{"a" => 1, "b" => 2, "c" => 3}, {"a" => 4, "b" => 5, "c" => 6}] - CSV.open( @path, "wb", headers: "b|a|c", - write_headers: true, - col_sep: "|" ) do |csv| - lines.each { |line| csv << line } - end - - # test writing fields in the correct order - File.open(@path, "rb") do |f| - assert_equal("b|a|c", f.gets.strip) - assert_equal("2|1|3", f.gets.strip) - assert_equal("5|4|6", f.gets.strip) - end - - # test reading CSV with headers - CSV.open( @path, "rb", headers: true, - col_sep: "|", - converters: :all ) do |csv| - csv.each { |line| assert_equal(lines.shift, line.to_hash) } - end - end - - def test_append # aliased add_row() and puts() - File.unlink(@path) - - CSV.open(@path, "wb", col_sep: "\t", row_sep: "\r\n") do |csv| - @expected.each { |row| csv << row } - end - - test_shift - - # same thing using CSV::Row objects - File.unlink(@path) - - CSV.open(@path, "wb", col_sep: "\t", row_sep: "\r\n") do |csv| - @expected.each { |row| csv << CSV::Row.new(Array.new, row) } - end - - test_shift - end - - ### Test Read and Write Interface ### - - def test_filter - assert_respond_to(CSV, :filter) - - expected = [[1, 2, 3], [4, 5]] - CSV.filter( "1;2;3\n4;5\n", (result = String.new), - in_col_sep: ";", out_col_sep: ",", - converters: :all ) do |row| - assert_equal(row, expected.shift) - row.map! { |n| n * 2 } - row << "Added\r" - end - assert_equal("2,4,6,\"Added\r\"\n8,10,\"Added\r\"\n", result) - end - - def test_instance - csv = String.new - - first = nil - assert_nothing_raised(Exception) do - first = CSV.instance(csv, col_sep: ";") - first << %w{a b c} - end - - assert_equal("a;b;c\n", csv) - - second = nil - assert_nothing_raised(Exception) do - second = CSV.instance(csv, col_sep: ";") - second << [1, 2, 3] - end - - assert_equal(first.object_id, second.object_id) - assert_equal("a;b;c\n1;2;3\n", csv) - - # shortcuts - assert_equal(STDOUT, CSV.instance.instance_eval { @io }) - assert_equal(STDOUT, CSV { |new_csv| new_csv.instance_eval { @io } }) - end - - def test_options_are_not_modified - opt = {}.freeze - assert_nothing_raised { CSV.foreach(@path, opt) } - assert_nothing_raised { CSV.open(@path, opt){} } - assert_nothing_raised { CSV.parse("", opt) } - assert_nothing_raised { CSV.parse_line("", opt) } - assert_nothing_raised { CSV.read(@path, opt) } - assert_nothing_raised { CSV.readlines(@path, opt) } - assert_nothing_raised { CSV.table(@path, opt) } - assert_nothing_raised { CSV.generate(opt){} } - assert_nothing_raised { CSV.generate_line([], opt) } - assert_nothing_raised { CSV.filter("", "", opt){} } - assert_nothing_raised { CSV.instance("", opt) } - end -end diff --git a/test/csv/test_patterns.rb b/test/csv/test_patterns.rb new file mode 100644 index 0000000000..881f03a3a4 --- /dev/null +++ b/test/csv/test_patterns.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require_relative "helper" + +class TestCSVPatternMatching < Test::Unit::TestCase + + def test_hash + case CSV::Row.new(%i{A B C}, [1, 2, 3]) + in B: b, C: c + assert_equal([2, 3], [b, c]) + end + end + + def test_hash_rest + case CSV::Row.new(%i{A B C}, [1, 2, 3]) + in B: b, **rest + assert_equal([2, { A: 1, C: 3 }], [b, rest]) + end + end + + def test_array + case CSV::Row.new(%i{A B C}, [1, 2, 3]) + in *, matched + assert_equal(3, matched) + end + end +end diff --git a/test/csv/test_row.rb b/test/csv/test_row.rb index 3acceea234..b717945041 100755..100644 --- a/test/csv/test_row.rb +++ b/test/csv/test_row.rb @@ -1,15 +1,9 @@ -#!/usr/bin/env ruby -w -# encoding: UTF-8 +# -*- coding: utf-8 -*- +# frozen_string_literal: false -# tc_row.rb -# -# Created by James Edward Gray II on 2005-10-31. -# Copyright 2005 James Edward Gray II. You can redistribute or modify this code -# under the terms of Ruby's license. +require_relative "helper" -require_relative "base" - -class TestCSV::Row < TestCSV +class TestCSVRow < Test::Unit::TestCase extend DifferentOFS def setup @@ -64,6 +58,9 @@ class TestCSV::Row < TestCSV # by index assert_equal(3, @row.field(2)) + # by range + assert_equal([2,3], @row.field(1..2)) + # missing assert_nil(@row.field("Missing")) assert_nil(@row.field(10)) @@ -103,6 +100,19 @@ class TestCSV::Row < TestCSV def test_has_key? assert_equal(true, @row.has_key?('B')) assert_equal(false, @row.has_key?('foo')) + + # aliases + assert_equal(true, @row.header?('B')) + assert_equal(false, @row.header?('foo')) + + assert_equal(true, @row.include?('B')) + assert_equal(false, @row.include?('foo')) + + assert_equal(true, @row.member?('B')) + assert_equal(false, @row.member?('foo')) + + assert_equal(true, @row.key?('B')) + assert_equal(false, @row.key?('foo')) end def test_set_field @@ -205,9 +215,20 @@ class TestCSV::Row < TestCSV # by header assert_equal(["C", 3], @row.delete("C")) - # using a block + end + + def test_delete_if assert_equal(@row, @row.delete_if { |h, f| h == "A" and not f.nil? }) - assert_equal([["A", nil]], @row.to_a) + assert_equal([["B", 2], ["C", 3], ["A", nil]], @row.to_a) + end + + def test_delete_if_without_block + enum = @row.delete_if + assert_instance_of(Enumerator, enum) + assert_equal(@row.size, enum.size) + + assert_equal(@row, enum.each { |h, f| h == "A" and not f.nil? }) + assert_equal([["B", 2], ["C", 3], ["A", nil]], @row.to_a) end def test_fields @@ -248,12 +269,6 @@ class TestCSV::Row < TestCSV end def test_queries - # headers - assert_send([@row, :header?, "A"]) - assert_send([@row, :header?, "C"]) - assert_not_send([@row, :header?, "Z"]) - assert_send([@row, :include?, "A"]) # alias - # fields assert(@row.field?(4)) assert(@row.field?(nil)) @@ -277,6 +292,27 @@ class TestCSV::Row < TestCSV # verify that we can chain the call assert_equal(@row, @row.each { }) + + # without block + ary = @row.to_a + enum = @row.each + assert_instance_of(Enumerator, enum) + assert_equal(@row.size, enum.size) + enum.each do |pair| + assert_equal(ary.first.first, pair.first) + assert_equal(ary.shift.last, pair.last) + end + end + + def test_each_pair + assert_equal([ + ["A", 1], + ["B", 2], + ["C", 3], + ["A", 4], + ["A", nil], + ], + @row.each_pair.to_a) end def test_enumerable @@ -298,7 +334,7 @@ class TestCSV::Row < TestCSV def test_to_hash hash = @row.to_hash - assert_equal({"A" => nil, "B" => 2, "C" => 3}, hash) + assert_equal({"A" => @row["A"], "B" => @row["B"], "C" => @row["C"]}, hash) hash.keys.each_with_index do |string_key, h| assert_predicate(string_key, :frozen?) assert_same(string_key, @row.headers[h]) @@ -352,4 +388,48 @@ class TestCSV::Row < TestCSV r = @row == [] assert_equal false, r end + + def test_dig_by_index + assert_equal(2, @row.dig(1)) + + assert_nil(@row.dig(100)) + end + + def test_dig_by_header + assert_equal(2, @row.dig("B")) + + assert_nil(@row.dig("Missing")) + end + + def test_dig_cell + row = CSV::Row.new(%w{A}, [["foo", ["bar", ["baz"]]]]) + + assert_equal("foo", row.dig(0, 0)) + assert_equal("bar", row.dig(0, 1, 0)) + + assert_equal("foo", row.dig("A", 0)) + assert_equal("bar", row.dig("A", 1, 0)) + end + + def test_dig_cell_no_dig + row = CSV::Row.new(%w{A}, ["foo"]) + + assert_raise(TypeError) do + row.dig(0, 0) + end + assert_raise(TypeError) do + row.dig("A", 0) + end + end + + def test_dup + row = CSV::Row.new(["A"], ["foo"]) + dupped_row = row.dup + dupped_row["A"] = "bar" + assert_equal(["foo", "bar"], + [row["A"], dupped_row["A"]]) + dupped_row.delete("A") + assert_equal(["foo", nil], + [row["A"], dupped_row["A"]]) + end end diff --git a/test/csv/test_table.rb b/test/csv/test_table.rb index 44e9d4a8be..e8ab74044e 100755..100644 --- a/test/csv/test_table.rb +++ b/test/csv/test_table.rb @@ -1,15 +1,9 @@ -#!/usr/bin/env ruby -w -# encoding: UTF-8 +# -*- coding: utf-8 -*- +# frozen_string_literal: false -# tc_table.rb -# -# Created by James Edward Gray II on 2005-10-31. -# Copyright 2005 James Edward Gray II. You can redistribute or modify this code -# under the terms of Ruby's license. +require_relative "helper" -require_relative "base" - -class TestCSV::Table < TestCSV +class TestCSVTable < Test::Unit::TestCase extend DifferentOFS def setup @@ -22,9 +16,11 @@ class TestCSV::Table < TestCSV @header_table = CSV::Table.new( [CSV::Row.new(%w{A B C}, %w{A B C}, true)] + @rows ) + + @header_only_table = CSV::Table.new([], headers: %w{A B C}) end - def test_initialze + def test_initialize assert_not_nil(@table) assert_instance_of(CSV::Table, @table) end @@ -43,6 +39,11 @@ class TestCSV::Table < TestCSV assert_equal(:row, rows.mode) assert_equal(@table, rows) + col_or_row = rows.by_col_or_row + assert_equal(:row, rows.mode) + assert_equal(:col_or_row, col_or_row.mode) + assert_equal(@table, col_or_row) + # destructive mode changing calls assert_equal(@table, @table.by_row!) assert_equal(:row, @table.mode) @@ -59,6 +60,17 @@ class TestCSV::Table < TestCSV assert_equal Array.new, t.headers end + def test_headers_only + assert_equal(%w[A B C], @header_only_table.headers) + end + + def test_headers_modified_by_row + table = CSV::Table.new([], headers: ["A", "B"]) + table << ["a", "b"] + table.first << {"C" => "c"} + assert_equal(["A", "B", "C"], table.headers) + end + def test_index ################## ### Mixed Mode ### @@ -67,6 +79,9 @@ class TestCSV::Table < TestCSV @rows.each_index { |i| assert_equal(@rows[i], @table[i]) } assert_equal(nil, @table[100]) # empty row + # by row with Range + assert_equal([@table[1], @table[2]], @table[1..2]) + # by col @rows.first.headers.each do |header| assert_equal(@rows.map { |row| row[header] }, @table[header]) @@ -146,13 +161,13 @@ class TestCSV::Table < TestCSV @table.to_a ) # verify resulting table - assert_equal(<<-END_RESULT.gsub(/^\s+/, ""), @table.to_csv) - A,B,C,Type,Index - 1,100,3,data,1 - 4,200,6,data,2 - 10,,12,data,3 - 13,,15,data, - END_RESULT + assert_equal(<<-CSV, @table.to_csv) +A,B,C,Type,Index +1,100,3,data,1 +4,200,6,data,2 +10,,12,data,3 +13,,15,data, + CSV # with headers @header_table["Type"] = "data" @@ -216,6 +231,17 @@ class TestCSV::Table < TestCSV # verify that we can chain the call assert_equal(@table, @table.each { }) + # without block + enum = @table.each + assert_instance_of(Enumerator, enum) + assert_equal(@table.size, enum.size) + + i = 0 + enum.each do |row| + assert_equal(@rows[i], row) + i += 1 + end + ################### ### Column Mode ### ################### @@ -227,6 +253,17 @@ class TestCSV::Table < TestCSV assert_equal(@table[header], column) end + # without block + enum = @table.each + assert_instance_of(Enumerator, enum) + assert_equal(@table.headers.size, enum.size) + + headers = @table.headers + enum.each do |header, column| + assert_equal(headers.shift, header) + assert_equal(@table[header], column) + end + ############################ ### One Shot Mode Change ### ############################ @@ -237,6 +274,31 @@ class TestCSV::Table < TestCSV @table.each { |row| assert_instance_of(CSV::Row, row) } end + def test_each_by_col_duplicated_headers + table = CSV.parse(<<-CSV, headers: true) +a,a,,,b +1,2,3,4,5 +11,12,13,14,15 + CSV + assert_equal([ + ["a", ["1", "11"]], + ["a", ["2", "12"]], + [nil, ["3", "13"]], + [nil, ["4", "14"]], + ["b", ["5", "15"]], + ], + table.by_col.each.to_a) + end + + def test_each_split + yielded_values = [] + @table.each do |column1, column2, column3| + yielded_values << [column1, column2, column3] + end + assert_equal(@rows.collect(&:to_a), + yielded_values) + end + def test_enumerable assert_equal( @rows.values_at(0, 2), @table.select { |row| (row["B"] % 2).zero? } ) @@ -253,12 +315,12 @@ class TestCSV::Table < TestCSV end def test_to_csv - csv = <<-END_CSV.gsub(/^\s+/, "") - A,B,C - 1,2,3 - 4,5,6 - 7,8,9 - END_CSV + csv = <<-CSV +A,B,C +1,2,3 +4,5,6 +7,8,9 + CSV # normal conversion assert_equal(csv, @table.to_csv) @@ -274,6 +336,43 @@ class TestCSV::Table < TestCSV assert_equal(csv, @header_table.to_csv) end + def test_to_csv_limit_positive + assert_equal(<<-CSV, @table.to_csv(limit: 2)) +A,B,C +1,2,3 +4,5,6 + CSV + end + + def test_to_csv_limit_positive_over + assert_equal(<<-CSV, @table.to_csv(limit: 5)) +A,B,C +1,2,3 +4,5,6 +7,8,9 + CSV + end + + def test_to_csv_limit_zero + assert_equal(<<-CSV, @table.to_csv(limit: 0)) +A,B,C + CSV + end + + def test_to_csv_limit_negative + assert_equal(<<-CSV, @table.to_csv(limit: -2)) +A,B,C +1,2,3 +4,5,6 + CSV + end + + def test_to_csv_limit_negative_over + assert_equal(<<-CSV, @table.to_csv(limit: -5)) +A,B,C + CSV + end + def test_append # verify that we can chain the call assert_equal(@table, @table << [10, 11, 12]) @@ -286,7 +385,7 @@ class TestCSV::Table < TestCSV assert_equal(CSV::Row.new(%w[A B C], [13, 14, 15]), @table[-1]) end - def test_delete_mixed + def test_delete_mixed_one ################## ### Mixed Mode ### ################## @@ -297,11 +396,33 @@ class TestCSV::Table < TestCSV assert_equal(@rows.map { |row| row["A"] }, @table.delete("A")) # verify resulting table - assert_equal(<<-END_RESULT.gsub(/^\s+/, ""), @table.to_csv) - B,C - 2,3 - 8,9 - END_RESULT + assert_equal(<<-CSV, @table.to_csv) +B,C +2,3 +8,9 + CSV + end + + def test_delete_mixed_multiple + ################## + ### Mixed Mode ### + ################## + # delete row and col + second_row = @rows[1] + a_col = @rows.map { |row| row["A"] } + a_col_without_second_row = a_col[0..0] + a_col[2..-1] + assert_equal([ + second_row, + a_col_without_second_row, + ], + @table.delete(1, "A")) + + # verify resulting table + assert_equal(<<-CSV, @table.to_csv) +B,C +2,3 +8,9 + CSV end def test_delete_column @@ -314,12 +435,12 @@ class TestCSV::Table < TestCSV assert_equal(@rows.map { |row| row["C"] }, @table.delete("C")) # verify resulting table - assert_equal(<<-END_RESULT.gsub(/^\s+/, ""), @table.to_csv) - B - 2 - 5 - 8 - END_RESULT + assert_equal(<<-CSV, @table.to_csv) +B +2 +5 +8 + CSV end def test_delete_row @@ -332,11 +453,11 @@ class TestCSV::Table < TestCSV assert_raise(TypeError) { @table.delete("C") } # verify resulting table - assert_equal(<<-END_RESULT.gsub(/^\s+/, ""), @table.to_csv) - A,B,C - 1,2,3 - 7,8,9 - END_RESULT + assert_equal(<<-CSV, @table.to_csv) +A,B,C +1,2,3 +7,8,9 + CSV end def test_delete_with_blank_rows @@ -353,10 +474,28 @@ class TestCSV::Table < TestCSV assert_equal(@table, @table.delete_if { |row| (row["B"] % 2).zero? }) # verify resulting table - assert_equal(<<-END_RESULT.gsub(/^\s+/, ""), @table.to_csv) - A,B,C - 4,5,6 - END_RESULT + assert_equal(<<-CSV, @table.to_csv) +A,B,C +4,5,6 + CSV + end + + def test_delete_if_row_without_block + ###################### + ### Mixed/Row Mode ### + ###################### + enum = @table.delete_if + assert_instance_of(Enumerator, enum) + assert_equal(@table.size, enum.size) + + # verify that we can chain the call + assert_equal(@table, enum.each { |row| (row["B"] % 2).zero? }) + + # verify resulting table + assert_equal(<<-CSV, @table.to_csv) +A,B,C +4,5,6 + CSV end def test_delete_if_column @@ -366,12 +505,46 @@ class TestCSV::Table < TestCSV @table.by_col! assert_equal(@table, @table.delete_if { |h, v| h > "A" }) - assert_equal(<<-END_RESULT.gsub(/^\s+/, ""), @table.to_csv) - A - 1 - 4 - 7 - END_RESULT + assert_equal(<<-CSV, @table.to_csv) +A +1 +4 +7 + CSV + end + + def test_delete_if_column_without_block + ################### + ### Column Mode ### + ################### + @table.by_col! + + enum = @table.delete_if + assert_instance_of(Enumerator, enum) + assert_equal(@table.headers.size, enum.size) + + assert_equal(@table, enum.each { |h, v| h > "A" }) + assert_equal(<<-CSV, @table.to_csv) +A +1 +4 +7 + CSV + end + + def test_delete_headers_only + ################### + ### Column Mode ### + ################### + @header_only_table.by_col! + + # delete by index + assert_equal([], @header_only_table.delete(0)) + assert_equal(%w[B C], @header_only_table.headers) + + # delete by header + assert_equal([], @header_only_table.delete("C")) + assert_equal(%w[B], @header_only_table.headers) end def test_values_at @@ -429,6 +602,90 @@ class TestCSV::Table < TestCSV assert_send([Encoding, :compatible?, Encoding.find("US-ASCII"), @table.inspect.encoding], - "inspect() was not ASCII compatible." ) + "inspect() was not ASCII compatible." ) + end + + def test_inspect_with_rows + additional_rows = [ CSV::Row.new(%w{A B C}, [101, 102, 103]), + CSV::Row.new(%w{A B C}, [104, 105, 106]), + CSV::Row.new(%w{A B C}, [107, 108, 109]) ] + table = CSV::Table.new(@rows + additional_rows) + str_table = table.inspect + + assert_equal(<<-CSV, str_table) +#<CSV::Table mode:col_or_row row_count:7> +A,B,C +1,2,3 +4,5,6 +7,8,9 +101,102,103 +104,105,106 + CSV + end + + def test_dig_mixed + # by row + assert_equal(@rows[0], @table.dig(0)) + assert_nil(@table.dig(100)) # empty row + + # by col + assert_equal([2, 5, 8], @table.dig("B")) + assert_equal([nil] * @rows.size, @table.dig("Z")) # empty col + + # by row then col + assert_equal(2, @table.dig(0, 1)) + assert_equal(6, @table.dig(1, "C")) + + # by col then row + assert_equal(5, @table.dig("B", 1)) + assert_equal(9, @table.dig("C", 2)) + end + + def test_dig_by_column + @table.by_col! + + assert_equal([2, 5, 8], @table.dig(1)) + assert_equal([2, 5, 8], @table.dig("B")) + + # by col then row + assert_equal(5, @table.dig("B", 1)) + assert_equal(9, @table.dig("C", 2)) + end + + def test_dig_by_row + @table.by_row! + + assert_equal(@rows[1], @table.dig(1)) + assert_raise(TypeError) { @table.dig("B") } + + # by row then col + assert_equal(2, @table.dig(0, 1)) + assert_equal(6, @table.dig(1, "C")) + end + + def test_dig_cell + table = CSV::Table.new([CSV::Row.new(["A"], [["foo", ["bar", ["baz"]]]])]) + + # by row, col then cell + assert_equal("foo", table.dig(0, "A", 0)) + assert_equal(["baz"], table.dig(0, "A", 1, 1)) + + # by col, row then cell + assert_equal("foo", table.dig("A", 0, 0)) + assert_equal(["baz"], table.dig("A", 0, 1, 1)) + end + + def test_dig_cell_no_dig + table = CSV::Table.new([CSV::Row.new(["A"], ["foo"])]) + + # by row, col then cell + assert_raise(TypeError) do + table.dig(0, "A", 0) + end + + # by col, row then cell + assert_raise(TypeError) do + table.dig("A", 0, 0) + end end end diff --git a/test/csv/ts_all.rb b/test/csv/ts_all.rb deleted file mode 100644 index 389384168b..0000000000 --- a/test/csv/ts_all.rb +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env ruby -w -# encoding: UTF-8 - -# ts_all.rb -# -# Created by James Edward Gray II on 2005-10-31. -# Copyright 2005 James Edward Gray II. You can redistribute or modify this code -# under the terms of Ruby's license. - -require "test/unit" - -require "test_csv_parsing" -require "test_features" -require "test_interface" -require "test_csv_writing" -require "test_data_converters" -require "test_row" -require "test_table" -require "test_headers" -require "test_encodings" diff --git a/test/csv/write/test_converters.rb b/test/csv/write/test_converters.rb new file mode 100644 index 0000000000..0e0080b4c5 --- /dev/null +++ b/test/csv/write/test_converters.rb @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# frozen_string_literal: false + +require_relative "../helper" + +module TestCSVWriteConverters + def test_one + assert_equal(%Q[=a,=b,=c\n], + generate_line(["a", "b", "c"], + write_converters: ->(value) {"=" + value})) + end + + def test_multiple + assert_equal(%Q[=a_,=b_,=c_\n], + generate_line(["a", "b", "c"], + write_converters: [ + ->(value) {"=" + value}, + ->(value) {value + "_"}, + ])) + end + + def test_nil_value + assert_equal(%Q[a,NaN,29\n], + generate_line(["a", nil, 29], + write_nil_value: "NaN")) + end + + def test_empty_value + assert_equal(%Q[a,,29\n], + generate_line(["a", "", 29], + write_empty_value: nil)) + end +end + +class TestCSVWriteConvertersGenerateLine < Test::Unit::TestCase + include TestCSVWriteConverters + extend DifferentOFS + + def generate_line(row, **kwargs) + CSV.generate_line(row, **kwargs) + end +end + +class TestCSVWriteConvertersGenerate < Test::Unit::TestCase + include TestCSVWriteConverters + extend DifferentOFS + + def generate_line(row, **kwargs) + CSV.generate(**kwargs) do |csv| + csv << row + end + end +end diff --git a/test/csv/write/test_force_quotes.rb b/test/csv/write/test_force_quotes.rb new file mode 100644 index 0000000000..622dcb021b --- /dev/null +++ b/test/csv/write/test_force_quotes.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: false + +require_relative "../helper" + +module TestCSVWriteForceQuotes + def test_default + assert_equal(%Q[1,2,3#{$INPUT_RECORD_SEPARATOR}], + generate_line(["1", "2", "3"])) + end + + def test_true + assert_equal(%Q["1","2","3"#{$INPUT_RECORD_SEPARATOR}], + generate_line(["1", "2", "3"], + force_quotes: true)) + end + + def test_false + assert_equal(%Q[1,2,3#{$INPUT_RECORD_SEPARATOR}], + generate_line(["1", "2", "3"], + force_quotes: false)) + end + + def test_field_name + assert_equal(%Q["1",2,"3"#{$INPUT_RECORD_SEPARATOR}], + generate_line(["1", "2", "3"], + headers: ["a", "b", "c"], + force_quotes: ["a", :c])) + end + + def test_field_name_without_headers + force_quotes = ["a", "c"] + error = assert_raise(ArgumentError) do + generate_line(["1", "2", "3"], + force_quotes: force_quotes) + end + assert_equal(":headers is required when you use field name " + + "in :force_quotes: " + + "#{force_quotes.first.inspect}: #{force_quotes.inspect}", + error.message) + end + + def test_field_index + assert_equal(%Q["1",2,"3"#{$INPUT_RECORD_SEPARATOR}], + generate_line(["1", "2", "3"], + force_quotes: [0, 2])) + end + + def test_field_unknown + force_quotes = [1.1] + error = assert_raise(ArgumentError) do + generate_line(["1", "2", "3"], + force_quotes: force_quotes) + end + assert_equal(":force_quotes element must be field index or field name: " + + "#{force_quotes.first.inspect}: #{force_quotes.inspect}", + error.message) + end +end + +class TestCSVWriteForceQuotesGenerateLine < Test::Unit::TestCase + include TestCSVWriteForceQuotes + extend DifferentOFS + + def generate_line(row, **kwargs) + CSV.generate_line(row, **kwargs) + end +end + +class TestCSVWriteForceQuotesGenerate < Test::Unit::TestCase + include TestCSVWriteForceQuotes + extend DifferentOFS + + def generate_line(row, **kwargs) + CSV.generate(**kwargs) do |csv| + csv << row + end + end +end diff --git a/test/csv/write/test_general.rb b/test/csv/write/test_general.rb new file mode 100644 index 0000000000..677119e1ae --- /dev/null +++ b/test/csv/write/test_general.rb @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- +# frozen_string_literal: false + +require_relative "../helper" + +module TestCSVWriteGeneral + include Helper + + def test_tab + assert_equal("\t#{$INPUT_RECORD_SEPARATOR}", + generate_line(["\t"])) + end + + def test_quote_character + assert_equal(%Q[foo,"""",baz#{$INPUT_RECORD_SEPARATOR}], + generate_line(["foo", %Q["], "baz"])) + end + + def test_quote_character_double + assert_equal(%Q[foo,"""""",baz#{$INPUT_RECORD_SEPARATOR}], + generate_line(["foo", %Q[""], "baz"])) + end + + def test_quote + assert_equal(%Q[foo,"""bar""",baz#{$INPUT_RECORD_SEPARATOR}], + generate_line(["foo", %Q["bar"], "baz"])) + end + + def test_quote_lf + assert_equal(%Q["""\n","""\n"#{$INPUT_RECORD_SEPARATOR}], + generate_line([%Q["\n], %Q["\n]])) + end + + def test_quote_cr + assert_equal(%Q["""\r","""\r"#{$INPUT_RECORD_SEPARATOR}], + generate_line([%Q["\r], %Q["\r]])) + end + + def test_quote_last + assert_equal(%Q[foo,"bar"""#{$INPUT_RECORD_SEPARATOR}], + generate_line(["foo", %Q[bar"]])) + end + + def test_quote_lf_last + assert_equal(%Q[foo,"\nbar"""#{$INPUT_RECORD_SEPARATOR}], + generate_line(["foo", %Q[\nbar"]])) + end + + def test_quote_lf_value_lf + assert_equal(%Q[foo,"""\nbar\n"""#{$INPUT_RECORD_SEPARATOR}], + generate_line(["foo", %Q["\nbar\n"]])) + end + + def test_quote_lf_value_lf_nil + assert_equal(%Q[foo,"""\nbar\n""",#{$INPUT_RECORD_SEPARATOR}], + generate_line(["foo", %Q["\nbar\n"], nil])) + end + + def test_cr + assert_equal(%Q[foo,"\r",baz#{$INPUT_RECORD_SEPARATOR}], + generate_line(["foo", "\r", "baz"])) + end + + def test_lf + assert_equal(%Q[foo,"\n",baz#{$INPUT_RECORD_SEPARATOR}], + generate_line(["foo", "\n", "baz"])) + end + + def test_cr_lf + assert_equal(%Q[foo,"\r\n",baz#{$INPUT_RECORD_SEPARATOR}], + generate_line(["foo", "\r\n", "baz"])) + end + + def test_cr_dot_lf + assert_equal(%Q[foo,"\r.\n",baz#{$INPUT_RECORD_SEPARATOR}], + generate_line(["foo", "\r.\n", "baz"])) + end + + def test_cr_lf_cr + assert_equal(%Q[foo,"\r\n\r",baz#{$INPUT_RECORD_SEPARATOR}], + generate_line(["foo", "\r\n\r", "baz"])) + end + + def test_cr_lf_lf + assert_equal(%Q[foo,"\r\n\n",baz#{$INPUT_RECORD_SEPARATOR}], + generate_line(["foo", "\r\n\n", "baz"])) + end + + def test_cr_lf_comma + assert_equal(%Q["\r\n,"#{$INPUT_RECORD_SEPARATOR}], + generate_line(["\r\n,"])) + end + + def test_cr_lf_comma_nil + assert_equal(%Q["\r\n,",#{$INPUT_RECORD_SEPARATOR}], + generate_line(["\r\n,", nil])) + end + + def test_comma + assert_equal(%Q[","#{$INPUT_RECORD_SEPARATOR}], + generate_line([","])) + end + + def test_comma_double + assert_equal(%Q[",",","#{$INPUT_RECORD_SEPARATOR}], + generate_line([",", ","])) + end + + def test_comma_and_value + assert_equal(%Q[foo,"foo,bar",baz#{$INPUT_RECORD_SEPARATOR}], + generate_line(["foo", "foo,bar", "baz"])) + end + + def test_one_element + assert_equal(%Q[foo#{$INPUT_RECORD_SEPARATOR}], + generate_line(["foo"])) + end + + def test_nil_values_only + assert_equal(%Q[,,#{$INPUT_RECORD_SEPARATOR}], + generate_line([nil, nil, nil])) + end + + def test_nil_double_only + assert_equal(%Q[,#{$INPUT_RECORD_SEPARATOR}], + generate_line([nil, nil])) + end + + def test_nil_values + assert_equal(%Q[foo,,,#{$INPUT_RECORD_SEPARATOR}], + generate_line(["foo", nil, nil, nil])) + end + + def test_nil_value_first + assert_equal(%Q[,foo,baz#{$INPUT_RECORD_SEPARATOR}], + generate_line([nil, "foo", "baz"])) + end + + def test_nil_value_middle + assert_equal(%Q[foo,,baz#{$INPUT_RECORD_SEPARATOR}], + generate_line(["foo", nil, "baz"])) + end + + def test_nil_value_last + assert_equal(%Q[foo,baz,#{$INPUT_RECORD_SEPARATOR}], + generate_line(["foo", "baz", nil])) + end + + def test_nil_empty + assert_equal(%Q[,""#{$INPUT_RECORD_SEPARATOR}], + generate_line([nil, ""])) + end + + def test_nil_cr + assert_equal(%Q[,"\r"#{$INPUT_RECORD_SEPARATOR}], + generate_line([nil, "\r"])) + end + + def test_values + assert_equal(%Q[foo,bar#{$INPUT_RECORD_SEPARATOR}], + generate_line(["foo", "bar"])) + end + + def test_semi_colon + assert_equal(%Q[;#{$INPUT_RECORD_SEPARATOR}], + generate_line([";"])) + end + + def test_semi_colon_values + assert_equal(%Q[;,;#{$INPUT_RECORD_SEPARATOR}], + generate_line([";", ";"])) + end + + def test_tab_values + assert_equal(%Q[\t,\t#{$INPUT_RECORD_SEPARATOR}], + generate_line(["\t", "\t"])) + end + + def test_col_sep + assert_equal(%Q[a;b;;c#{$INPUT_RECORD_SEPARATOR}], + generate_line(["a", "b", nil, "c"], + col_sep: ";")) + assert_equal(%Q[a\tb\t\tc#{$INPUT_RECORD_SEPARATOR}], + generate_line(["a", "b", nil, "c"], + col_sep: "\t")) + end + + def test_row_sep + assert_equal(%Q[a,b,,c\r\n], + generate_line(["a", "b", nil, "c"], + row_sep: "\r\n")) + end + + def test_force_quotes + assert_equal(%Q["1","b","","already ""quoted"""#{$INPUT_RECORD_SEPARATOR}], + generate_line([1, "b", nil, %Q{already "quoted"}], + force_quotes: true)) + end + + def test_encoding_utf8 + assert_equal(%Q[あ,い,う#{$INPUT_RECORD_SEPARATOR}], + generate_line(["あ" , "い", "う"])) + end + + def test_encoding_euc_jp + row = ["あ", "い", "う"].collect {|field| field.encode("EUC-JP")} + assert_equal(%Q[あ,い,う#{$INPUT_RECORD_SEPARATOR}].encode("EUC-JP"), + generate_line(row)) + end + + def test_encoding_with_default_internal + with_default_internal(Encoding::UTF_8) do + row = ["あ", "い", "う"].collect {|field| field.encode("EUC-JP")} + assert_equal(%Q[あ,い,う#{$INPUT_RECORD_SEPARATOR}].encode("EUC-JP"), + generate_line(row, encoding: Encoding::EUC_JP)) + end + end + + def test_with_default_internal + with_default_internal(Encoding::UTF_8) do + row = ["あ", "い", "う"].collect {|field| field.encode("EUC-JP")} + assert_equal(%Q[あ,い,う#{$INPUT_RECORD_SEPARATOR}].encode("EUC-JP"), + generate_line(row)) + end + end +end + +class TestCSVWriteGeneralGenerateLine < Test::Unit::TestCase + include TestCSVWriteGeneral + extend DifferentOFS + + def generate_line(row, **kwargs) + CSV.generate_line(row, **kwargs) + end +end + +class TestCSVWriteGeneralGenerate < Test::Unit::TestCase + include TestCSVWriteGeneral + extend DifferentOFS + + def generate_line(row, **kwargs) + CSV.generate(**kwargs) do |csv| + csv << row + end + end +end diff --git a/test/csv/write/test_quote_empty.rb b/test/csv/write/test_quote_empty.rb new file mode 100644 index 0000000000..70f73dad4a --- /dev/null +++ b/test/csv/write/test_quote_empty.rb @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# frozen_string_literal: false + +require_relative "../helper" + +module TestCSVWriteQuoteEmpty + def test_quote_empty_default + assert_equal(%Q["""",""#{$INPUT_RECORD_SEPARATOR}], + generate_line([%Q["], ""])) + end + + def test_quote_empty_false + assert_equal(%Q["""",#{$INPUT_RECORD_SEPARATOR}], + generate_line([%Q["], ""], + quote_empty: false)) + end + + def test_empty_default + assert_equal(%Q[foo,"",baz#{$INPUT_RECORD_SEPARATOR}], + generate_line(["foo", "", "baz"])) + end + + def test_empty_false + assert_equal(%Q[foo,,baz#{$INPUT_RECORD_SEPARATOR}], + generate_line(["foo", "", "baz"], + quote_empty: false)) + end + + def test_empty_only_default + assert_equal(%Q[""#{$INPUT_RECORD_SEPARATOR}], + generate_line([""])) + end + + def test_empty_only_false + assert_equal(%Q[#{$INPUT_RECORD_SEPARATOR}], + generate_line([""], + quote_empty: false)) + end + + def test_empty_double_default + assert_equal(%Q["",""#{$INPUT_RECORD_SEPARATOR}], + generate_line(["", ""])) + end + + def test_empty_double_false + assert_equal(%Q[,#{$INPUT_RECORD_SEPARATOR}], + generate_line(["", ""], + quote_empty: false)) + end +end + +class TestCSVWriteQuoteEmptyGenerateLine < Test::Unit::TestCase + include TestCSVWriteQuoteEmpty + extend DifferentOFS + + def generate_line(row, **kwargs) + CSV.generate_line(row, **kwargs) + end +end + +class TestCSVWriteQuoteEmptyGenerate < Test::Unit::TestCase + include TestCSVWriteQuoteEmpty + extend DifferentOFS + + def generate_line(row, **kwargs) + CSV.generate(**kwargs) do |csv| + csv << row + end + end +end |
