escapeCell left '=x as it was and unescapeCell then dropped its apostrophe on import. A value of apostrophes followed by a formula character now gets one more apostrophe on export, and the import drops exactly one, so unescapeCell is the inverse of escapeCell.
122 lines
4.2 KiB
Go
122 lines
4.2 KiB
Go
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)
|
|
}
|
|
}
|
|
})
|
|
}
|