package classes import ( "errors" "reflect" "strings" "testing" ) // TestCSV: the export writes a BOM, the delimiter and escaped formula // cells; the import strips the BOM, detects or takes the delimiter, reads // quoting, unescapes formula cells and refuses what is not CSV. func TestCSV(t *testing.T) { header := []string{"source", "target", "note"} rows := [][]string{ {"/a", "/b", "plain"}, {"/ł", "/ż", "zażółć; gęślą, jaźń"}, {"/q", "/r", `she said "hi"` + "\nnext line"}, {"/f", "/g", "=HYPERLINK(\"x\")"}, {"/p", "/s", "+1"}, {"/m", "/n", "-1"}, {"/at", "/x", "@SUM(A1)"}, {"/tab", "/x", "\tlead"}, {"/cr", "/x", "\rlead"}, {"/ok", "/x", "a=b"}, {"/ap", "/x", "'=x"}, {"/ap2", "/x", "''-)"}, {"/ap3", "/x", "'plain"}, } t.Run("round trip with each delimiter", func(t *testing.T) { for _, delim := range []string{",", ";", ""} { out, err := WriteCSV(header, rows, delim) if err != nil { t.Fatal(err) } if !strings.HasPrefix(out, BOM) { t.Fatalf("%q: no BOM", delim) } sep := "," if delim == ";" { sep = ";" } if !strings.HasPrefix(strings.TrimPrefix(out, BOM), strings.Join(header, sep)+"\n") { t.Fatalf("%q: header line %q", delim, strings.SplitN(out, "\n", 2)[0]) } gotHeader, got, err := ReadCSV(out, delim) if err != nil || !reflect.DeepEqual(gotHeader, header) || len(got) != len(rows) { t.Fatalf("%q: ReadCSV = %v, %d rows, %v", delim, gotHeader, len(got), err) } for i, row := range rows { // Escaped cells keep their leading tab or CR: the apostrophe // protects them from the import's trimming. for j, col := range header { if want := row[j]; got[i].Fields[col] != want { t.Fatalf("%q row %d %s = %q, want %q", delim, i, col, got[i].Fields[col], want) } } } } }) t.Run("formula cells are escaped on export", func(t *testing.T) { out, _ := WriteCSV([]string{"note"}, [][]string{{"=1+1"}, {"+x"}, {"-x"}, {"@x"}, {"a=b"}, {""}}, ",") lines := strings.Split(strings.TrimPrefix(out, BOM), "\n") if lines[1] != "'=1+1" || lines[2] != "'+x" || lines[3] != "'-x" || lines[4] != "'@x" || lines[5] != "a=b" { t.Fatalf("export = %q", lines) } for in, want := range map[string]string{"'=x": "=x", "'+x": "+x", "'abc": "'abc", "''=x": "'=x", "plain": "plain", "'": "'"} { if got := unescapeCell(in); got != want { t.Errorf("unescapeCell(%q) = %q, want %q", in, got, want) } } }) t.Run("delimiter detection and the setting", func(t *testing.T) { if CSVDelimiter(";") != ';' || CSVDelimiter(" ; ") != ';' || CSVDelimiter(",") != ',' || CSVDelimiter("") != ',' || CSVDelimiter("|") != ',' { t.Fatal("CSVDelimiter") } h, rows, err := ReadCSV("Source;Target\n/a;/b\n", "") if err != nil || !reflect.DeepEqual(h, []string{"source", "target"}) || rows[0].Fields["target"] != "/b" { t.Fatalf("detected ; = %v %+v %v", h, rows, err) } h, rows, err = ReadCSV(BOM+" SOURCE , Target \n/a,/b\n", "") if err != nil || !reflect.DeepEqual(h, []string{"source", "target"}) || rows[0].Line != 2 { t.Fatalf("detected , = %v %+v %v", h, rows, err) } }) t.Run("quoting, blank rows and extra cells", func(t *testing.T) { data := "source,target,note\n\"/a,1\",\"/b\"\"x\",\"two\nlines\"\n,,\n\n/c,/d,n,extra\n" _, rows, err := ReadCSV(data, ",") if err != nil || len(rows) != 2 { t.Fatalf("rows = %+v, %v", rows, err) } if rows[0].Fields["source"] != "/a,1" || rows[0].Fields["target"] != `/b"x` || rows[0].Fields["note"] != "two\nlines" || rows[0].Line != 2 { t.Fatalf("quoted row = %+v", rows[0]) } if rows[1].Fields["source"] != "/c" || len(rows[1].Fields) != 3 || rows[1].Line != 6 { t.Fatalf("row with an extra cell = %+v", rows[1]) } }) t.Run("unreadable files", func(t *testing.T) { for name, data := range map[string]string{ "empty": "", "a BOM only": BOM, "blank": " \n\t\n", "a bare quote": "source,target\n\"/a,/b\n", "the wrong delimiter": "source;target\n/a;/b\n", } { delim := "" if name == "the wrong delimiter" { delim = "," } if _, _, err := ReadCSV(data, delim); !errors.Is(err, ErrUnreadable) { t.Errorf("%s: err = %v", name, err) } } }) }