From f81f79c88bbb46297dcf7640285bd0d11bff8988 Mon Sep 17 00:00:00 2001 From: Daniil Medvedev <medvdanil@gmail.com> Date: Mon, 20 Jul 2015 17:10:57 +0300 Subject: [PATCH] csv tests reorganization --- csv.documentation | 63 --------------- src/lib/csv/csv.c | 16 +++- src/lib/csv/csv.h | 19 +++-- src/lua/csv.lua | 61 +++++++------- test/app/csv.result | 76 +++-------------- test/app/csv.test.lua | 46 +++++------ test/unit/csv.c | 82 ++++++++++++++----- test/unit/csv.result | 184 ++++++++++++------------------------------ 8 files changed, 202 insertions(+), 345 deletions(-) delete mode 100644 csv.documentation diff --git a/csv.documentation b/csv.documentation deleted file mode 100644 index 912f78b48d..0000000000 --- a/csv.documentation +++ /dev/null @@ -1,63 +0,0 @@ -Tarantool supports CSV file input/output. -CSV is comma separated values, like this: -example.txt: -package,method,return value -fio,pathjoin,string -csv,load,table -none,",comma in field", and ""quote"" - -Commas an linebreaks in fields must be in quotes -Quotes in fields is repeated two times quote character. -You can set delimiter and quote character: - csv.delimiter = ',' - csv.quote = '"' -Input/output works through readable/writable objects, for example files or sockets. -Readable object has method read(N), which returns N or less bytes as string. -Writable object has method write(string), which sends string to output. - -csv.iterate = function(readable[, csv_chunk_size]) ---@brief parse csv string by string ---@param readable must be string or object with method read(num) returns string ---@param csv_chunk_size (default 4096). Parser will read by csv_chunk_size symbols ---@return iter function, iterator state - -Example: - f = require("fio").open("example.txt", { "O_RDONLY"}) - for tup in csv.iterate(f) do - print(tup[1], tup[2], tup[3]) - end -Output: - package method return value - fio pathjoin string - csv load table - none ,comma in field and "quote" - -csv.load = function(readable[, skip_lines, csv_chunk_size]) ---@brief parse csv and make table ---@param skip_lines is number of lines to skip. ---@return table -If csv file has a header, it may be skipped. - -csv.dump = function(t[, writable]) ---@brief dumps tuple or table as csv ---@param t is table or tuple. Fields may be of any type, but it will be converted to string by method tostring(field). ---@param writable must be object with method write(string) like file or socket ---@return there is no writable it returns csv as string - -Example: - f = require("fio").open("dump.csv", { "O_WRONLY", "O_TRUNC" , "O_CREAT"}, 0x1FF) - multiline_header = {{'csv example'}, {'3 numbers per string:'}} - csv.dump(multiline_header, f) - for i=0,14,3 do - t = {i, i + 1, i + 2} - s = csv.dump(t, f) - end -dump.csv: - csv example - 3 numbers per string: - 0,1,2 - 3,4,5 - 6,7,8 - 9,10,11 - 12,13,14 - diff --git a/src/lib/csv/csv.c b/src/lib/csv/csv.c index 25a8050eb4..9640a822e7 100644 --- a/src/lib/csv/csv.c +++ b/src/lib/csv/csv.c @@ -249,21 +249,29 @@ csv_feed(struct csv_iterator *it, const char *str) } int -csv_escape_field(struct csv *csv, const char *field, char *dst) +csv_escape_field(struct csv *csv, const char *field, size_t field_len, char *dst, size_t buf_size) { char *p = dst; int inquotes = 0; - if(strchr(field, csv->csv_delim) || strchr(field, '\n') || strchr(field, '\r')) { + if(memchr(field, csv->csv_delim, field_len) || memchr(field, '\n', field_len) || memchr(field, '\r', field_len)) { inquotes = 1; *p++ = csv->csv_quote; } while(*field) { - if(*field == csv->csv_quote) + if(*field == csv->csv_quote) { + if(p - dst >= buf_size) + return -1; *p++ = csv->csv_quote; + } + if(p - dst >= buf_size) + return -1; *p++ = *field++; } - if(inquotes) + if(inquotes) { + if(p - dst >= buf_size) + return -1; *p++ = csv->csv_quote; + } *p = 0; return p - dst; } diff --git a/src/lib/csv/csv.h b/src/lib/csv/csv.h index 6fd48b7caa..8faaadc85e 100644 --- a/src/lib/csv/csv.h +++ b/src/lib/csv/csv.h @@ -120,15 +120,24 @@ void csv_feed(struct csv_iterator *, const char *); /** - * @brief csv_escape_field adds pair quote and - * if there is comma or linebreak in field, adds surrounding quotes + * @brief csv_escape_field prepares field to out in file. + * Adds pair quote and if there is comma or linebreak in field, adds surrounding quotes. + * At worst escaped field will 2 times more symbols than input field. + * @return length of escaped field or -1 if not enough space in buffer. */ int -csv_escape_field(struct csv *csv, const char *field, char *dst); +csv_escape_field(struct csv *csv, const char *field, size_t field_len, char *dst, size_t buf_size); -#define CSV_ITERATOR_GET_FIELD(it) it->field -#define CSV_ITERATOR_GET_FLEN(it) it->field_len +static inline const char* csv_iterator_get_field(struct csv_iterator *it) +{ + return it->field; +} + +static inline size_t csv_iterator_get_field_len(struct csv_iterator *it) +{ + return it->field_len; +} #if defined(__cplusplus) } #endif /* extern "C" */ diff --git a/src/lua/csv.lua b/src/lua/csv.lua index 8c0183e7f7..0fd0b93edc 100644 --- a/src/lua/csv.lua +++ b/src/lua/csv.lua @@ -41,7 +41,7 @@ ffi.cdef[[ void csv_iter_create(struct csv_iterator *it, struct csv *csv); int csv_next(struct csv_iterator *); void csv_feed(struct csv_iterator *, const char *); - int csv_escape_field(struct csv *, const char *, char *); + int csv_escape_field(struct csv *csv, const char *field, size_t field_len, char *dst, size_t buf_size); enum { CSV_IT_OK, CSV_IT_EOL, @@ -51,7 +51,28 @@ ffi.cdef[[ }; ]] -local iter = function(csvstate) +local make_readable = function(s) + rd = {} + rd.val = s + rd.read = function(self, cnt) + local res = self.val; + self.val = "" + return res + end + return rd +end + +local make_writable = function() + wr = {} + wr.returnstring = "" + wr.write = function(self, s) + wr.returnstring = wr.returnstring .. s + end + return wr +end + + +local iter = function(csvstate, i) local readable = csvstate[1] local csv_chunk_size = csvstate[2] local csv = csvstate[3] @@ -62,7 +83,8 @@ local iter = function(csvstate) if st == ffi.C.CSV_IT_NEEDMORE then ffi.C.csv_feed(it, readable:read(csv_chunk_size)) elseif st == ffi.C.CSV_IT_EOL then - return tup + i = i + 1 + return i, tup elseif st == ffi.C.CSV_IT_OK then table.insert(tup, ffi.string(it[0].field, it[0].field_len)) elseif st == ffi.C.CSV_IT_ERROR then @@ -76,26 +98,6 @@ local iter = function(csvstate) end end -local make_readable = function(s) - rd = {} - rd.val = s - rd.read = function(self, cnt) - local res = self.val; - self.val = "" - return res - end - return rd -end - -local make_writable = function() - wr = {} - wr.returnstring = "" - wr.write = function(self, s) - wr.returnstring = wr.returnstring .. s - end - return wr -end - local module = {} module.delimiter = ',' @@ -126,7 +128,7 @@ module.iterate = function(readable, csv_chunk_size) ffi.C.csv_iter_create(it, csv) ffi.C.csv_feed(it, str) - return iter, {readable, csv_chunk_size, csv, it} + return iter, {readable, csv_chunk_size, csv, it}, 0 end --@brief parse csv and make table @@ -136,12 +138,9 @@ module.load = function(readable, skip_lines, csv_chunk_size) skip_lines = skip_lines or 0 csv_chunk_size = csv_chunk_size or 4096 result = {} - i = 0 - for tup in module.iterate(readable, csv_chunk_size) do - if i < skip_lines then - i = i + 1 - else - table.insert(result, tup) + for i, tup in module.iterate(readable, csv_chunk_size) do + if i > skip_lines then + result[i - skip_lines] = tup end end @@ -175,7 +174,7 @@ module.dump = function(t, writable) bufsz = (strf:len() + 1) * 2 buf = csv[0].csv_realloc(buf, bufsz) end - local len = ffi.C.csv_escape_field(csv, strf, buf) + local len = ffi.C.csv_escape_field(csv, strf, string.len(strf), buf, bufsz) if first then first = false else diff --git a/test/app/csv.result b/test/app/csv.result index 73f58e2b21..bb6183e414 100644 --- a/test/app/csv.result +++ b/test/app/csv.result @@ -1,66 +1,10 @@ -obj test1: -|a| |b| -|1| |ha -"ha" -ha| -|3| |4| - -obj test2: -|| || || -|| || -|| - -obj test3: -|| || -|kp"v| - -fio test1: -|123| |5| |92| |0| |0| -|1| |12 34| |56| |quote , | |66| -|ok| - -fio test2: -|1| -|23| |456| |abcac| |'multiword field 4'| -|none| |none| |0| -|| || || -|aba| |adda| |f3| |0| -|local res = internal.pwrite(self.fh| |data| |len| |offset)| -|iflag = bit.bor(iflag| |fio.c.flag[ flag ])| -|| || || - -fio test3: -|23| |456| |abcac| |'multiword field 4'| -|none| |none| |0| -|| || || -|aba| |adda| |f3| |0| -|local res = internal.pwrite(self.fh| |data| |len| |offset)| -|iflag = bit.bor(iflag| |fio.c.flag[ flag ])| -|| || || - -test roundtrip: -true -test iterate, only first field: -1 -23 -none - -aba -local res = internal.pwrite(self.fh -iflag = bit.bor(iflag - -test str dump: -quote"" d,",and, comma","both "" of "" t,h,e,m" -"""""",","",""" -"mul -ti -li -ne - -",field - -"" -" -" - -test load(dump(t)): true +TAP version 13 +1..8 +ok - obj test1 +ok - obj test2 +ok - obj test3 +ok - fio test1 +ok - fio test2 +ok - fio test3 +ok - test roundtrip +ok - test load(dump(t)) diff --git a/test/app/csv.test.lua b/test/app/csv.test.lua index 3bb6a54bf4..06ebaefec4 100755 --- a/test/app/csv.test.lua +++ b/test/app/csv.test.lua @@ -18,40 +18,49 @@ local function myread(self, bytes) end local csv = require('csv') local fio = require('fio') +local tap = require('tap') +local test1 = '|a|\t|b|\t\n|1|\t|ha\n"ha"\nha|\t\n|3|\t|4|\t\n' +local test2 = '||\t||\t||\t\n||\t||\t\n||\t\n' +local test3 = '||\t||\t\n|kp"v|\t\n' +local test4 = '|123|\t|5|\t|92|\t|0|\t|0|\t\n|1|\t|12 34|\t|56|\t|quote , |\t|66|\t\n|ok|\t\n' +local test5 = "|1|\t\n|23|\t|456|\t|abcac|\t|'multiword field 4'|\t\n|none|\t|none|\t|0|\t\n" .. + "||\t||\t||\t\n|aba|\t|adda|\t|f3|\t|0|\t\n|local res = internal.pwrite(self.fh|\t|d" .. + "ata|\t|len|\t|offset)|\t\n|iflag = bit.bor(iflag|\t|fio.c.flag[ flag ])|\t\n||\t||\t||\t\n" +local test6 = "|23|\t|456|\t|abcac|\t|'multiword field 4'|\t\n|none|\t|none|\t|0|\t\n||\t||\t||\t\n" .. + "|aba|\t|adda|\t|f3|\t|0|\t\n|local res = internal.pwrite(self.fh|\t|data|\t|len|\t|offset)" .. + "|\t\n|iflag = bit.bor(iflag|\t|fio.c.flag[ flag ])|\t\n||\t||\t||\t\n" + +test = tap.test("csv") +test:plan(8) -print("obj test1:") readable = {} readable.read = myread readable.v = "a,b\n1,\"ha\n\"\"ha\"\"\nha\"\n3,4\n" readable.i = 0 -print(table2str(csv.load(readable))) +test:is(table2str(csv.load(readable)), test1, "obj test1") -print("obj test2:") readable.v = ", ,\n , \n\n" readable.i = 0 -print(table2str(csv.load(readable, 0, 1))) +test:is(table2str(csv.load(readable, 0, 1)), test2, "obj test2") -print("obj test3:") readable.v = ", \r\nkp\"\"v" readable.i = 0 -print(table2str(csv.load(readable, 0, 3))) +test:is(table2str(csv.load(readable, 0, 3)), test3, "obj test3") tmpdir = fio.tempdir() file1 = fio.pathjoin(tmpdir, 'file.1') file2 = fio.pathjoin(tmpdir, 'file.2') file3 = fio.pathjoin(tmpdir, 'file.3') -print("fio test1:") local f = fio.open(file1, { 'O_WRONLY', 'O_TRUNC', 'O_CREAT' }, 0777) f:write("123 , 5 , 92 , 0, 0\n" .. "1, 12 34, 56, \"quote , \", 66\nok") f:close() f = fio.open(file1, {'O_RDONLY'}) -print(table2str(csv.load(f,0,10))) +test:is(table2str(csv.load(f,0,10)), test4, "fio test1") f:close() -print("fio test2:") f = fio.open(file2, { 'O_WRONLY', 'O_TRUNC', 'O_CREAT' }, 0777) f:write("1\n23,456,abcac,\'multiword field 4\'\n" .. "none,none,0\n" .. @@ -63,12 +72,11 @@ f:write("1\n23,456,abcac,\'multiword field 4\'\n" .. ) f:close() f = fio.open(file2, {'O_RDONLY'}) -print(table2str(csv.load(f, 0, 1))) --symbol by symbol reading +test:is(table2str(csv.load(f, 0, 1)), test5, "fio test2") --symbol by symbol reading f:close() -print("fio test3:") f = fio.open(file2, {'O_RDONLY'}) -print(table2str(csv.load(f, 1, 7))) --7 symbols per chunk +test:is(table2str(csv.load(f, 1, 7)), test6, "fio test3") --7 symbols per chunk f:close() @@ -88,19 +96,9 @@ f = fio.open(file3, {'O_RDONLY'}) t2 = csv.load(f, 0, 5) f:close() -print("test roundtrip: ") -print(table2str(t) == table2str(t2)) +test:is(table2str(t), table2str(t2), "test roundtrip") -print("test iterate, only first field:") -f = fio.open(file2, {'O_RDONLY'}) -for tup in csv.iterate(f) do - print(tup[1]) -end -f:close() - -print("test str dump:") -print(csv.dump(t)) -print("test load(dump(t)): " .. tostring(table2str(t) == table2str(csv.load(csv.dump(t))))) +test:is(table2str(t), table2str(csv.load(csv.dump(t))), "test load(dump(t))") fio.unlink(file1) fio.unlink(file2) diff --git a/test/unit/csv.c b/test/unit/csv.c index 8a36c7b6b6..6863254967 100644 --- a/test/unit/csv.c +++ b/test/unit/csv.c @@ -4,15 +4,20 @@ #include <string.h> #include <assert.h> +int isendl = 1; void print_endl(void *ctx) { fflush(stdout); puts(""); + isendl = 1; } void print_field(void *ctx, const char *s, const char *end) { + if(!isendl) + putchar('\t'); + isendl = 0; putchar('|'); for(const char *p = s; p != end && *p; p++) { if((*p == '\r' || *p == '\n') && (p + 1 == end || (*(p + 1) != '\r' && *(p + 1) != '\n'))) @@ -21,9 +26,26 @@ print_field(void *ctx, const char *s, const char *end) putchar(*p); } putchar('|'); - putchar('\t'); fflush(stdout); } +void +buf_endl(void *ctx) +{ + *(*((char**)ctx))++ = '\n'; +} +void +buf_field(void *ctx, const char *s, const char *end) +{ + *(*((char**)ctx))++ = '|'; + for(const char *p = s; p != end && *p; p++) { + if((*p == '\r' || *p == '\n') && (p + 1 == end || (*(p + 1) != '\r' && *(p + 1) != '\n'))) + *(*((char**)ctx))++ = '\n'; + else + *(*((char**)ctx))++ = *p; + } + *(*((char**)ctx))++ = '|'; + *(*((char**)ctx))++ = '\t'; +} void small_string_test(const char* const s) { @@ -33,7 +55,6 @@ void small_string_test(const char* const s) csv.emit_row = print_endl; csv_parse_chunk(&csv, s, s + strlen(s)); csv_finish_parsing(&csv); - printf("valid: %s\n", csv.csv_invalid ? "NO" : "yes"); csv_destroy(&csv); } @@ -172,18 +193,33 @@ void big_chunk_separated_test() { void random_generated_test() { header(); - small_string_test( - "\n\r\" ba\r a\ra, \n\"\n\"a\nb\" \raa\rb,\n" - "\r, \n\",\r\n\"\n,a, ,\"a\n\n\r \"\r ba\r,b" - " a,\n,\"\"a\n\r \"b\" \n,\",a\r,a ,\r\rc" - "\" a,b\r\n,\"b\r\"aa \nb \n\r\r\n\n,\rb\nc" - ",\n\n aa\n \"\n ab\rab,\r\" b\n\", ,,\r\r" - "bab\rb\na\n\"a\ra,\"\",\n\"a\n\n \"\r \ra\n" - "a\r\raa a\" ,baab ,a \rbb ,\r \r,\rb,, b" - "\n\r\"\nb\n\nb \n,ab \raa\r\"\nb a\"ba,b, c" - "\"a\"a \"\r\n\"b \n,b\"\",\nba\n\" \n\na \r" - "\nb\rb\"bbba,\" \n\n\n,a,b,a,b,\n\n\n\nb\"\r" - ); + const char *rand_test = + "\n\r\" ba\r a\ra, \n\"\n\"a\nb\" \raa\rb,\n" + "\r, \n\",\r\n\"\n,a, ,\"a\n\n\r \"\r ba\r,b" + " a,\n,\"\"a\n\r \"b\" \n,\",a\r,a ,\r\rc" + "\" a,b\r\n,\"b\r\"aa \nb \n\r\r\n\n,\rb\nc" + ",\n\n aa\n \"\n ab\rab,\r\" b\n\", ,,\r\r" + "bab\rb\na\n\"a\ra,\"\",\n\"a\n\n \"\r \ra\n" + "a\r\raa a\" ,baab ,a \rbb ,\r \r,\rb,, b" + "\n\r\"\nb\n\nb \n,ab \raa\r\"\nb a\"ba,b, c" + "\"a\"a \"\r\n\"b \n,b\"\",\nba\n\" \n\na \r" + "\nb\rb\"bbba,\" \n\n\n,a,b,a,b,\n\n\n\nb\"\r"; + + struct csv csv; + csv_create(&csv); + csv_setopt(&csv, CSV_OPT_EMIT_FIELD, fieldsizes_counter); + csv_setopt(&csv, CSV_OPT_EMIT_ROW, line_counter); + + struct counter cnt; + cnt.line_cnt = 0; + cnt.fieldsizes_cnt = 0; + csv_setopt(&csv, CSV_OPT_CTX, &cnt); + + csv_parse_chunk(&csv, rand_test, rand_test + strlen(rand_test)); + csv_finish_parsing(&csv); + printf("line_cnt=%d, fieldsizes_cnt=%d\n", (int)cnt.line_cnt, (int)cnt.fieldsizes_cnt); + printf("valid: %s\n", csv.csv_invalid ? "NO" : "yes"); + csv_destroy(&csv); footer(); } @@ -203,7 +239,7 @@ void iter_test1() { buf += strlen(buf); break; case CSV_IT_EOL: - printf("\n"); + print_endl(0); break; case CSV_IT_OK: print_field(0, it.field, it.field + it.field_len); @@ -233,7 +269,7 @@ void iter_test2() { buf += 3; break; case CSV_IT_EOL: - printf("\n"); + print_endl(0); break; case CSV_IT_OK: print_field(0, it.field, it.field + it.field_len); @@ -250,14 +286,18 @@ void iter_test2() { void csv_out() { header(); - const char fields[4][36] = { "abc", "with,comma", "\"in quotes\"", "1 \" quote"}; - char buf[18]; + const char fields[5][24] = { "abc", "with,comma", "\"in quotes\"", "1 \" quote", + "long field, return \"-1\"" }; + char buf[24]; int i; struct csv csv; csv_create(&csv); - for(i = 0; i < 4; i++) { - int len = csv_escape_field(&csv, fields[i], buf); - printf("%s<len=%d>%c", buf, len, i == 3 ? '\n' : ','); + for(i = 0; i < 5; i++) { + int len = csv_escape_field(&csv, fields[i], strlen(fields[i]), buf, sizeof(buf)); + if(len != -1) + printf("%s<len=%d>%c", buf, len, i == 4 ? '\n' : ','); + else + printf("<len=%d>%c", len, i == 4 ? '\n' : ','); } footer(); diff --git a/test/unit/csv.result b/test/unit/csv.result index 8d56fa9744..61a66cb1a0 100644 --- a/test/unit/csv.result +++ b/test/unit/csv.result @@ -1,189 +1,111 @@ *** test1 *** -|1| -|| -|1| |2| |3| -|123| +|1| +|| +|1| |2| |3| +|123| valid: yes *** test1: done *** *** test2 *** -|123| |456| |abcac| |'multiword field 4'| -|none| |none| |0| -|| || || -|| || || +|123| |456| |abcac| |'multiword field 4'| +|none| |none| |0| +|| || || +|| || || valid: yes *** test2: done *** *** test3 *** -|1| || |2| +|1| || |2| valid: yes *** test3: done *** *** test4 *** -|123| |5| |92| |0| |0| -|1| |12 34| |56| |quote , | |66| -|ok| +|123| |5| |92| |0| |0| +|1| |12 34| |56| |quote , | |66| +|ok| valid: yes *** test4: done *** *** test5 *** -|abc| |longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong| |0| -|123| |456| || -|0| || || +|abc| |longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong| |0| +|123| |456| || +|0| || || valid: yes *** test5: done *** *** test6 *** -|| -|| -|abc| -|c"| |d| |de| -|| -|k| -|e| -|| -|| -| | +|| +|| +|abc| +|c"| |d| |de| +|| +|k| +|e| +|| +|| +| | valid: NO *** test6: done *** *** big_chunk_separated_test *** line_cnt=10000, fieldsizes_cnt=1920000, 1920000 *** big_chunk_separated_test: done *** *** random_generated_test *** -|| -| ba - a -a, -| -|a -b| -|aa| -|b| || -|| || -|, -| -|| |a| || |a - - - | -|ba| -|| |b a| || -|| |"a| -|b| -|| |,a -,a , -c a| |b| -|| |b -aa| -|b| -|| -|| -|| || -|b| -|c| || -|| -|aa| -| - ab -ab, - b| -|, ,, -bab -b -a -a| -|a| |"| || -|a - - | -|| -|a| -|a| -|| -|aa a ,baab ,a -bb , - -, -b,, b - -| -|b| -|| -|b| -|| |ab| -|aa| -| -b aba| |b| |caa -b| -|| |b"| || -|ba| -| - -a -b -bbbba| | - - -,a,b,a,b, - - - -b| +line_cnt=40, fieldsizes_cnt=183 valid: yes *** random_generated_test: done *** *** common_test *** -|first| |last| |address| |city| |zip| -|John| |Doe| |120 any st.| |Anytown, WW| |08123| +|first| |last| |address| |city| |zip| +|John| |Doe| |120 any st.| |Anytown, WW| |08123| valid: yes *** common_test: done *** *** common_test *** -|a| |b| |c| -|1| |"| |"| -|2| |3| |4| +|a| |b| |c| +|1| |"| |"| +|2| |3| |4| valid: yes *** common_test: done *** *** common_test *** -|a| |b| -|1| |ha "ha" ha| -|3| |4| +|a| |b| +|1| |ha "ha" ha| +|3| |4| valid: yes *** common_test: done *** *** common_test *** -|key| |val| -|1| |{"type": "Point", "coordinates": [102.0, 0.5]}| +|key| |val| +|1| |{"type": "Point", "coordinates": [102.0, 0.5]}| valid: yes *** common_test: done *** *** common_test *** -|a| |b| |c| -|1| |2| |3| +|a| |b| |c| +|1| |2| |3| |Once upon -a time| |5| |6| -|7| |8| |9| +a time| |5| |6| +|7| |8| |9| valid: yes *** common_test: done *** *** common_test *** -|a| |b| +|a| |b| |1| |ha "ha" -ha| -|3| |4| +ha| +|3| |4| valid: yes *** common_test: done *** *** common_test *** -|a| |b| |c| -|1| |2| |3| -|4| |5| |а нет ли ошибок?| +|a| |b| |c| +|1| |2| |3| +|4| |5| |а нет ли ошибок?| valid: yes *** common_test: done *** *** common_test *** -|www| |aaa| |tt | +|www| |aaa| |tt | valid: yes *** common_test: done *** *** iter_test1 *** -|| |d| |e| -|12| |42| |3| -|o| +|| |d| |e| +|12| |42| |3| +|o| *** iter_test1: done *** *** iter_test2 *** -|1| -|23| +|1| +|23| *** iter_test2: done *** *** csv_out *** -abc<len=3>,"with,comma"<len=12>,""in quotes""<len=13>,1 "" quote<len=10> +abc<len=3>,"with,comma"<len=12>,""in quotes""<len=13>,1 "" quote<len=10>,<len=-1> *** csv_out: done *** \ No newline at end of file -- GitLab