Files
gocarina-gocsv/decode_test.go
T
dualfroz 26aafe16a8 fix: parse integer fields as base 10
toInt and toUint passed base 0 to strconv, which infers the base from the
literal the way the Go compiler does. A zero-padded CSV field was then
read as octal: 08 and 09 failed with invalid syntax and 010 silently
parsed as 8.

Pass base 10 at both call sites. Go-style prefixed literals such as 0x1f
are no longer accepted; they were undocumented and untested, and cannot
be kept without keeping the octal reading of 010 as well.

Closes #182
2026-09-04 07:41:12 +02:00

1392 lines
33 KiB
Go

package gocsv
import (
"bytes"
"encoding/csv"
"errors"
"io"
"reflect"
"strconv"
"strings"
"testing"
"time"
)
func Test_readTo(t *testing.T) {
blah := 0
sptr := "*string"
sptr2 := ""
b := bytes.NewBufferString(`foo,BAR,Baz,Blah,SPtr,Omit
f,1,baz,,*string,*string
e,3,b,,,`)
d := newSimpleDecoderFromReader(b)
var samples []Sample
if err := readTo(d, &samples); err != nil {
t.Fatal(err)
}
if len(samples) != 2 {
t.Fatalf("expected 2 sample instances, got %d", len(samples))
}
expected := Sample{Foo: "f", Bar: 1, Baz: "baz", Blah: &blah, SPtr: &sptr, Omit: &sptr}
if !reflect.DeepEqual(expected, samples[0]) {
t.Fatalf("expected first sample %v, got %v", expected, samples[0])
}
expected = Sample{Foo: "e", Bar: 3, Baz: "b", Blah: &blah, SPtr: &sptr2}
if !reflect.DeepEqual(expected, samples[1]) {
t.Fatalf("expected second sample %v, got %v", expected, samples[1])
}
b = bytes.NewBufferString(`foo,BAR,Baz
f,1,baz
e,BAD_INPUT,b`)
d = newSimpleDecoderFromReader(b)
samples = []Sample{}
err := readTo(d, &samples)
if err == nil {
t.Fatalf("Expected error from bad input, got: %+v", samples)
}
switch actualErr := err.(type) {
case *csv.ParseError:
if actualErr.Line != 3 {
t.Fatalf("Expected csv.ParseError on line 3, got: %d", actualErr.Line)
}
if actualErr.Column != 2 {
t.Fatalf("Expected csv.ParseError in column 2, got: %d", actualErr.Column)
}
default:
t.Fatalf("incorrect error type: %T", err)
}
}
func Test_readToNormalized(t *testing.T) {
SetHeaderNormalizer(func(s string) string {
return strings.ToLower(s)
})
defer SetHeaderNormalizer(DefaultNameNormalizer())
blah := 0
sptr := "*string"
sptr2 := ""
b := bytes.NewBufferString(`FOO,BAR,BAZ,BLAH,SPTR,OMIT
f,1,baz,,*string,*string
e,3,b,,,`)
d := newSimpleDecoderFromReader(b)
var samples []Sample
if err := readTo(d, &samples); err != nil {
t.Fatal(err)
}
if len(samples) != 2 {
t.Fatalf("expected 2 sample instances, got %d", len(samples))
}
expected := Sample{Foo: "f", Bar: 1, Baz: "baz", Blah: &blah, SPtr: &sptr, Omit: &sptr}
if !reflect.DeepEqual(expected, samples[0]) {
t.Fatalf("expected first sample %v, got %v", expected, samples[0])
}
expected = Sample{Foo: "e", Bar: 3, Baz: "b", Blah: &blah, SPtr: &sptr2}
if !reflect.DeepEqual(expected, samples[1]) {
t.Fatalf("expected second sample %v, got %v", expected, samples[1])
}
b = bytes.NewBufferString(`foo,BAR,Baz
f,1,baz
e,BAD_INPUT,b`)
d = newSimpleDecoderFromReader(b)
samples = []Sample{}
err := readTo(d, &samples)
if err == nil {
t.Fatalf("Expected error from bad input, got: %+v", samples)
}
switch actualErr := err.(type) {
case *csv.ParseError:
if actualErr.Line != 3 {
t.Fatalf("Expected csv.ParseError on line 3, got: %d", actualErr.Line)
}
if actualErr.Column != 2 {
t.Fatalf("Expected csv.ParseError in column 2, got: %d", actualErr.Column)
}
default:
t.Fatalf("incorrect error type: %T", err)
}
}
func Test_readTo_Time(t *testing.T) {
b := bytes.NewBufferString(`Foo
1970-01-01T03:01:00+03:00`)
d := newSimpleDecoderFromReader(b)
var samples []DateTime
if err := readTo(d, &samples); err != nil {
t.Fatal(err)
}
rt, _ := time.Parse(time.RFC3339, "1970-01-01T03:01:00+03:00")
expected := DateTime{Foo: rt}
if !reflect.DeepEqual(expected, samples[0]) {
t.Fatalf("expected first sample %v, got %v", expected, samples[0])
}
}
func Test_readTo_complex_embed(t *testing.T) {
b := bytes.NewBufferString(`first,foo,BAR,Baz,last,abc
aa,bb,11,cc,dd,ee
ff,gg,22,hh,ii,jj`)
d := newSimpleDecoderFromReader(b)
var samples []SkipFieldSample
if err := readTo(d, &samples); err != nil {
t.Fatal(err)
}
if len(samples) != 2 {
t.Fatalf("expected 2 sample instances, got %d", len(samples))
}
expected := SkipFieldSample{
EmbedSample: EmbedSample{
Qux: "aa",
Sample: Sample{
Foo: "bb",
Bar: 11,
Baz: "cc",
},
Quux: "dd",
},
Corge: "ee",
}
if expected != samples[0] {
t.Fatalf("expected first sample %v, got %v", expected, samples[0])
}
expected = SkipFieldSample{
EmbedSample: EmbedSample{
Qux: "ff",
Sample: Sample{
Foo: "gg",
Bar: 22,
Baz: "hh",
},
Quux: "ii",
},
Corge: "jj",
}
if expected != samples[1] {
t.Fatalf("expected first sample %v, got %v", expected, samples[1])
}
}
func Test_readTo_embed_ptr(t *testing.T) {
b := bytes.NewBufferString(`first,foo,BAR,Baz,last,abc
aa,bb,11,cc,dd,ee
ff,gg,22,hh,ii,jj`)
d := newSimpleDecoderFromReader(b)
var rows []EmbedPtrSample
if err := readTo(d, &rows); err != nil {
t.Fatalf(err.Error())
}
expected := EmbedPtrSample{
Qux: "ff",
Sample: &Sample{
Foo: "gg",
Bar: 22,
Baz: "hh",
},
Quux: "ii",
}
if !reflect.DeepEqual(expected, rows[1]) {
t.Fatalf("expected first sample %v, got %+v", expected, rows[1])
}
}
func Test_readTo_slice(t *testing.T) {
b := bytes.NewBufferString(`Slice
[]
[1, 2, 3]`)
reader := csv.NewReader(b)
reader.Comma = '\t'
d := csvDecoder{reader}
samples := []SliceSample{}
if err := readTo(d, &samples); err != nil {
t.Fatal(err)
}
expected := SliceSample{Slice: []int{}}
if !reflect.DeepEqual(expected, samples[0]) {
t.Fatalf("expected first sample %v, got %v", expected, samples[0].Slice)
}
expected = SliceSample{Slice: []int{1, 2, 3}}
if !reflect.DeepEqual(expected, samples[1]) {
t.Fatalf("expected second sample %v, got %v", expected, samples[1].Slice)
}
}
func Test_readTo_slice_structs(t *testing.T) {
b := bytes.NewBufferString(`s[0].string,slice[0].f,slice[1].s,s[1].float,a[0].s,array[0].float,a[1].s,array[1].float,ints[0],ints[1],ints[2]
s1,1.1,s2,2.2,s3,3.3,s4,4.4,1,2,3`)
d := newSimpleDecoderFromReader(b)
var samples []SliceStructSample
err := readTo(d, &samples)
if err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
expected := SliceStructSample{
Slice: []SliceStruct{
{String: "s1", Float: 1.1},
{String: "s2", Float: 2.2},
},
SimpleSlice: []int{1, 2, 3},
Array: [2]SliceStruct{
{String: "s3", Float: 3.3},
{String: "s4", Float: 4.4},
},
}
if !reflect.DeepEqual(expected, samples[0]) {
t.Fatalf("expected \n sample: %v\n got: %v", expected, samples[0])
}
}
func Test_readTo_embed_marshal(t *testing.T) {
b := bytes.NewBufferString(`foo
bar`)
d := newSimpleDecoderFromReader(b)
var rows []EmbedMarshal
if err := readTo(d, &rows); err != nil {
t.Fatalf(err.Error())
}
expected := EmbedMarshal{
Foo: &MarshalSample{Dummy: "bar"},
}
if !reflect.DeepEqual(expected, rows[0]) {
t.Fatalf("expected first sample %v, got %+v", expected, rows[1])
}
}
func Test_readTo_embed_unmarshal_csv_with_clashing_field(t *testing.T) {
b := bytes.NewBufferString(`Symbol,Timestamp
test,1656460798.693201614`)
d := newSimpleDecoderFromReader(b)
var rows []EmbedUnmarshalCSVWithClashingField
if err := readTo(d, &rows); err != nil {
t.Fatalf(err.Error())
}
expected := EmbedUnmarshalCSVWithClashingField{
Symbol: "test",
Timestamp: &UnmarshalCSVSample{Timestamp: 1656460798, Nanos: 693201614},
}
if !reflect.DeepEqual(expected, rows[0]) {
t.Fatalf("expected first sample %v, got %+v", expected, rows[0])
}
}
func Test_readEach(t *testing.T) {
b := bytes.NewBufferString(`first,foo,BAR,Baz,last,abc
aa,bb,11,cc,dd,ee
ff,gg,22,hh,ii,jj`)
d := newSimpleDecoderFromReader(b)
c := make(chan SkipFieldSample)
e := make(chan error)
var samples []SkipFieldSample
go func() {
if err := readEach(d, nil, c); err != nil {
e <- err
}
}()
L:
for {
select {
case err := <-e:
t.Fatal(err)
case v, ok := <-c:
if !ok {
break L
}
samples = append(samples, v)
}
}
if len(samples) != 2 {
t.Fatalf("expected 2 sample instances, got %d", len(samples))
}
expected := SkipFieldSample{
EmbedSample: EmbedSample{
Qux: "aa",
Sample: Sample{
Foo: "bb",
Bar: 11,
Baz: "cc",
},
Quux: "dd",
},
Corge: "ee",
}
if expected != samples[0] {
t.Fatalf("expected first sample %v, got %v", expected, samples[0])
}
expected = SkipFieldSample{
EmbedSample: EmbedSample{
Qux: "ff",
Sample: Sample{
Foo: "gg",
Bar: 22,
Baz: "hh",
},
Quux: "ii",
},
Corge: "jj",
}
if expected != samples[1] {
t.Fatalf("expected first sample %v, got %v", expected, samples[1])
}
}
func Test_readEach_withErrorHandler(t *testing.T) {
b := bytes.NewBufferString(`first,foo,BAR,Baz,last,abc
aa,bb,11,cc,dd,ee
ff,gg,22,hh,ii,jj
kk,ll,ab,mm,nn,oo
`)
d := newSimpleDecoderFromReader(b)
var errHandler ErrorHandler
errHandler = func(parseError *csv.ParseError) bool {
if parseError.Line != 4 {
t.Fatalf("expected parse error on line 4, got %d", parseError.Line)
}
if parseError.Column != 3 {
t.Fatalf("expected parse error on column 3, got %d", parseError.Column)
}
return false
}
c := make(chan SkipFieldSample)
e := make(chan error)
var samples []SkipFieldSample
go func() {
if err := readEach(d, errHandler, c); err != nil {
e <- err
}
}()
L:
for {
select {
case err := <-e:
t.Fatal(err)
case v, ok := <-c:
if !ok {
break L
}
samples = append(samples, v)
}
}
if len(samples) != 2 {
t.Fatalf("expected 2 sample instances, got %d", len(samples))
}
expected := SkipFieldSample{
EmbedSample: EmbedSample{
Qux: "aa",
Sample: Sample{
Foo: "bb",
Bar: 11,
Baz: "cc",
},
Quux: "dd",
},
Corge: "ee",
}
if expected != samples[0] {
t.Fatalf("expected first sample %v, got %v", expected, samples[0])
}
expected = SkipFieldSample{
EmbedSample: EmbedSample{
Qux: "ff",
Sample: Sample{
Foo: "gg",
Bar: 22,
Baz: "hh",
},
Quux: "ii",
},
Corge: "jj",
}
if expected != samples[1] {
t.Fatalf("expected first sample %v, got %v", expected, samples[1])
}
}
func Test_readEachWithoutHeaders(t *testing.T) {
blah := 0
sptr := ""
b := bytes.NewBufferString(`f,1,baz,1.66,,,
e,3,b,,,,`)
d := newSimpleDecoderFromReader(b)
c := make(chan Sample)
e := make(chan error)
var samples []Sample
go func() {
if err := readEachWithoutHeaders(d, c); err != nil {
e <- err
}
}()
L:
for {
select {
case err := <-e:
t.Fatal(err)
case v, ok := <-c:
if !ok {
break L
}
samples = append(samples, v)
}
}
if len(samples) != 2 {
t.Fatalf("expected 2 sample instances, got %d", len(samples))
}
expected := Sample{Foo: "f", Bar: 1, Baz: "baz", Frop: 1.66, Blah: &blah, SPtr: &sptr}
if !reflect.DeepEqual(expected, samples[0]) {
t.Fatalf("expected first sample %v, got %v", expected, samples[0])
}
expected = Sample{Foo: "e", Bar: 3, Baz: "b", Frop: 0, Blah: &blah, SPtr: &sptr}
if !reflect.DeepEqual(expected, samples[1]) {
t.Fatalf("expected second sample %v, got %v", expected, samples[1])
}
}
// Test_readEach_TypeUnmarshalCSVWithFields test that readEach works with type implementing TypeUnmarshalCSVWithFields for unmarshalling
func Test_readEach_TypeUnmarshalCSVWithFields(t *testing.T) {
b := bytes.NewBufferString(`foo,bar,baz,frop
bb,1,cc,3.14
gg,2,hh,4`)
d := newSimpleDecoderFromReader(b)
c := make(chan UnmarshalCSVWithFieldsSample)
e := make(chan error)
var samples []UnmarshalCSVWithFieldsSample
go func() {
if err := readEach(d, nil, c); err != nil {
e <- err
}
}()
L:
for {
select {
case err := <-e:
t.Fatal(err)
case v, ok := <-c:
if !ok {
break L
}
samples = append(samples, v)
}
}
if len(samples) != 2 {
t.Fatalf("expected 2 sample instances, got %d", len(samples))
}
expected := UnmarshalCSVWithFieldsSample{
Foo: "bb",
Bar: 1,
Baz: "cc",
Frop: 314,
}
if expected != samples[0] {
t.Fatalf("expected first sample %v, got %v", expected, samples[0])
}
expected = UnmarshalCSVWithFieldsSample{
Foo: "gg",
Bar: 2,
Baz: "hh",
Frop: 400,
}
if expected != samples[1] {
t.Fatalf("expected first sample %v, got %v", expected, samples[1])
}
}
func Test_maybeMissingStructFields(t *testing.T) {
structTags := []fieldInfo{
{keys: []string{"foo"}},
{keys: []string{"bar"}},
{keys: []string{"baz"}},
}
badHeaders := []string{"hi", "mom", "bacon"}
goodHeaders := []string{"foo", "bar", "baz"}
// no tags to match, expect no error
if err := maybeMissingStructFields([]fieldInfo{}, goodHeaders); err != nil {
t.Fatal(err)
}
// bad headers, expect an error
if err := maybeMissingStructFields(structTags, badHeaders); err == nil {
t.Fatal("expected an error, but no error found")
} else if !errors.Is(err, ErrUnmatchedStructTags) {
t.Fatal("expected ErrUnmatchedStructTags, but got a different error")
}
// good headers, expect no error
if err := maybeMissingStructFields(structTags, goodHeaders); err != nil {
t.Fatal(err)
}
// extra headers, but all structtags match; expect no error
moarHeaders := append(goodHeaders, "qux", "quux", "corge", "grault")
if err := maybeMissingStructFields(structTags, moarHeaders); err != nil {
t.Fatal(err)
}
// not all structTags match, but there's plenty o' headers; expect
// error
mismatchedHeaders := []string{"foo", "qux", "quux", "corgi"}
if err := maybeMissingStructFields(structTags, mismatchedHeaders); err == nil {
t.Fatal("expected an error, but no error found")
} else if !errors.Is(err, ErrUnmatchedStructTags) {
t.Fatal("expected ErrUnmatchedStructTags, but got a different error")
}
}
func Test_maybeDoubleHeaderNames(t *testing.T) {
b := bytes.NewBufferString(`foo,BAR,foo
f,1,baz
e,3,b`)
d := newSimpleDecoderFromReader(b)
var samples []Sample
// *** check maybeDoubleHeaderNames
if err := maybeDoubleHeaderNames([]string{"foo", "BAR", "foo"}); err == nil {
t.Fatal("maybeDoubleHeaderNames did not raise an error when a should have.")
} else if !errors.Is(err, ErrDoubleHeaderNames) {
t.Fatal("maybeDoubleHeaderNames did not raise an error of type ErrDoubleHeaderNames.")
}
// *** check readTo
if err := readTo(d, &samples); err != nil {
t.Fatal(err)
}
// Double header allowed, value should be of third row
if samples[0].Foo != "baz" {
t.Fatal("Double header allowed, value should be of third row but is not. Function called is readTo.")
}
b = bytes.NewBufferString(`foo,BAR,foo
f,1,baz
e,3,b`)
d = newSimpleDecoderFromReader(b)
ShouldAlignDuplicateHeadersWithStructFieldOrder = true
if err := readTo(d, &samples); err != nil {
t.Fatal(err)
}
// Double header allowed, value should be of first row
if samples[0].Foo != "f" {
t.Fatal("Double header allowed, value should be of first row but is not. Function called is readTo.")
}
ShouldAlignDuplicateHeadersWithStructFieldOrder = false
// Double header not allowed, should fail
FailIfDoubleHeaderNames = true
if err := readTo(d, &samples); err == nil {
t.Fatal("Double header not allowed but no error raised. Function called is readTo.")
}
// *** check readEach
FailIfDoubleHeaderNames = false
b = bytes.NewBufferString(`foo,BAR,foo
f,1,baz
e,3,b`)
d = newSimpleDecoderFromReader(b)
samples = samples[:0]
c := make(chan Sample)
e := make(chan error)
go func() {
if err := readEach(d, nil, c); err != nil {
e <- err
}
}()
L1:
for {
select {
case err := <-e:
t.Fatal(err)
case v, ok := <-c:
if !ok {
break L1
}
samples = append(samples, v)
}
}
// Double header allowed, value should be of third row
if samples[0].Foo != "baz" {
t.Fatal("Double header allowed, value should be of third row but is not. Function called is readEach.")
}
// Double header not allowed, should fail
FailIfDoubleHeaderNames = true
b = bytes.NewBufferString(`foo,BAR,foo
f,1,baz
e,3,b`)
d = newSimpleDecoderFromReader(b)
c = make(chan Sample)
e = make(chan error)
go func() {
if err := readEach(d, nil, c); err == nil {
e <- err
}
}()
L2:
for {
select {
case <-e:
t.Fatal("Double header not allowed but no error raised. Function called is readEach.")
case v, ok := <-c:
if !ok {
break L2
}
samples = append(samples, v)
}
}
}
func TestUnmarshalToCallback(t *testing.T) {
b := bytes.NewBufferString(`first,foo,BAR,Baz,last,abc
aa,bb,11,cc,dd,ee
ff,gg,22,hh,ii,jj`)
var samples []SkipFieldSample
if err := UnmarshalBytesToCallback(b.Bytes(), func(s SkipFieldSample) {
samples = append(samples, s)
}); err != nil {
t.Fatal(err)
}
if len(samples) != 2 {
t.Fatalf("expected 2 sample instances, got %d", len(samples))
}
expected := SkipFieldSample{
EmbedSample: EmbedSample{
Qux: "aa",
Sample: Sample{
Foo: "bb",
Bar: 11,
Baz: "cc",
},
Quux: "dd",
},
Corge: "ee",
}
if expected != samples[0] {
t.Fatalf("expected first sample %v, got %v", expected, samples[0])
}
expected = SkipFieldSample{
EmbedSample: EmbedSample{
Qux: "ff",
Sample: Sample{
Foo: "gg",
Bar: 22,
Baz: "hh",
},
Quux: "ii",
},
Corge: "jj",
}
if expected != samples[1] {
t.Fatalf("expected first sample %v, got %v", expected, samples[1])
}
}
// TestRenamedTypes tests for unmarshaling functions on redefined basic types.
func TestRenamedTypesUnmarshal(t *testing.T) {
// Set different csv field separator to enable comma in floats
SetCSVReader(func(in io.Reader) CSVReader {
csvin := csv.NewReader(in)
csvin.Comma = ';'
return csvin
})
b := bytes.NewBufferString(`foo;bar
1,4;1.5
2,3;2.4`)
d := newSimpleDecoderFromReader(b)
var samples []RenamedSample
// Switch back to default for tests executed after this
defer SetCSVReader(DefaultCSVReader)
if err := readTo(d, &samples); err != nil {
t.Fatal(err)
}
if samples[0].RenamedFloatUnmarshaler != 1.4 {
t.Fatalf("Parsed float value wrong for renamed float64 type. Expected 1.4, got %v.", samples[0].RenamedFloatUnmarshaler)
}
if samples[0].RenamedFloatDefault != 1.5 {
t.Fatalf("Parsed float value wrong for renamed float64 type without an explicit unmarshaler function. Expected 1.5, got %v.", samples[0].RenamedFloatDefault)
}
// Test that errors raised by UnmarshalCSV are correctly reported
b = bytes.NewBufferString(`foo;bar
4.2;2.4`)
d = newSimpleDecoderFromReader(b)
samples = samples[:0]
if perr, _ := readTo(d, &samples).(*csv.ParseError); perr == nil {
t.Fatalf("Expected ParseError, got nil.")
} else if _, ok := perr.Err.(UnmarshalError); !ok {
t.Fatalf("Expected UnmarshalError, got %v", perr.Err)
}
}
func (rf *RenamedFloat64Unmarshaler) UnmarshalCSV(csv string) (err error) {
// Purely for testing purposes: Raise error on specific string
if csv == "4.2" {
return UnmarshalError{"Test error: Invalid float 4.2"}
}
// Convert , to . before parsing to create valid float strings
converted := strings.Replace(csv, ",", ".", -1)
var f float64
if f, err = strconv.ParseFloat(converted, 64); err != nil {
return err
}
*rf = RenamedFloat64Unmarshaler(f)
return nil
}
// TestUnmarshalCSVWithFields test that the TestUnmarshalCSVWithFields interface to unmarshal all the fields works
func TestUnmarshalCSVWithFields(t *testing.T) {
b := []byte(`foo,bar,baz,frop
bar,1,zip,3.14
baz,2,zap,4.00`)
var samples []UnmarshalCSVWithFieldsSample
err := UnmarshalBytes(b, &samples)
if err != nil {
t.Fatalf("UnmarshalCSVWithFields() -> UnmarshalBytes() %v", err)
}
tests := []struct {
name string
index int
wantFoo string
wantBar int
wantBaz string
wantFrop float64
}{
{
name: "should validate index 0",
index: 0,
wantFoo: "bar",
wantBar: 1,
wantBaz: "zip",
wantFrop: 314,
},
{
name: "should validate index 1",
index: 1,
wantFoo: "baz",
wantBar: 2,
wantBaz: "zap",
wantFrop: 400,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if samples[tt.index].Foo != tt.wantFoo {
t.Fatalf("UnmarshalCSVWithFields() Index %d Foo expected %v got %v", tt.index, tt.wantFoo, samples[tt.index].Foo)
}
if samples[tt.index].Bar != tt.wantBar {
t.Fatalf("UnmarshalCSVWithFields() Index %d Bar expected %v got %v", tt.index, tt.wantBar, samples[tt.index].Bar)
}
if samples[tt.index].Baz != tt.wantBaz {
t.Fatalf("UnmarshalCSVWithFields() Index %d Baz expected %v got %v", tt.index, tt.wantBaz, samples[tt.index].Baz)
}
if samples[tt.index].Frop != tt.wantFrop {
t.Fatalf("UnmarshalCSVWithFields() Index %d Frop expected %v got %v", tt.index, tt.wantFrop, samples[tt.index].Frop)
}
})
}
}
func (u *UnmarshalCSVWithFieldsSample) UnmarshalCSVWithFields(key, value string) error {
switch key {
case "foo":
u.Foo = value
case "bar":
i, err := strconv.Atoi(value)
if err != nil {
return err
}
u.Bar = i
case "baz":
u.Baz = value
case "frop":
f, err := strconv.ParseFloat(value, 64)
if err != nil {
return err
}
u.Frop = f * 100
}
return nil
}
type UnmarshalError struct {
msg string
}
func (e UnmarshalError) Error() string {
return e.msg
}
// TestUnmarshalCSVWithFieldsNoTags test that the TypeUnmarshalCSVWithFields interface works for types without matching csv tags (or no csv tags at all)
func TestUnmarshalCSVWithFieldsNoTags(t *testing.T) {
b := []byte(`foo,bar,baz,frop
bar,1,zip,3.14
baz,2,zap,4.00`)
var samples []NoTagsSample
err := UnmarshalBytes(b, &samples)
if err != nil {
t.Fatalf("UnmarshalCSVWithFields() -> UnmarshalBytes() %v", err)
}
if len(samples) != 2 {
t.Fatalf("expected 2 sample instances, got %d", len(samples))
}
if len(samples[0].Fields) != 4 {
t.Fatalf("expected 4 sample fields in map, got %d", len(samples[0].Fields))
}
checkFieldMap := func(expected map[string]string, actual map[string]string) {
for expectedKey, expectedValue := range expected {
if sampleValue, ok := actual[expectedKey]; ok {
if sampleValue != expectedValue {
t.Fatalf("expected %s key in map to have %s value, got %s", expectedKey, expectedValue, sampleValue)
}
} else {
t.Fatalf("expected map to have key %s", expectedKey)
}
}
}
expectedFields := map[string]string{
"foo": "bar",
"bar": "1",
"baz": "zip",
"frop": "3.14",
}
checkFieldMap(expectedFields, samples[0].Fields)
expectedFields = map[string]string{
"foo": "baz",
"bar": "2",
"baz": "zap",
"frop": "4.00",
}
checkFieldMap(expectedFields, samples[1].Fields)
}
func (u *NoTagsSample) UnmarshalCSVWithFields(key, value string) error {
if u.Fields == nil {
u.Fields = map[string]string{}
}
u.Fields[key] = value
return nil
}
func TestMultipleStructTags(t *testing.T) {
b := bytes.NewBufferString(`foo,BAR,Baz
e,3,b`)
d := newSimpleDecoderFromReader(b)
var samples []MultiTagSample
if err := readTo(d, &samples); err != nil {
t.Fatal(err)
}
if samples[0].Foo != "b" {
t.Fatalf("expected second tag value 'b' in multi tag struct field, got %v", samples[0].Foo)
}
b = bytes.NewBufferString(`foo,BAR
e,3`)
d = newSimpleDecoderFromReader(b)
if err := readTo(d, &samples); err != nil {
t.Fatal(err)
}
if samples[0].Foo != "e" {
t.Fatalf("wrong value in multi tag struct field, expected 'e', got %v", samples[0].Foo)
}
b = bytes.NewBufferString(`BAR,Baz
3,b`)
d = newSimpleDecoderFromReader(b)
if err := readTo(d, &samples); err != nil {
t.Fatal(err)
}
if samples[0].Foo != "b" {
t.Fatal("wrong value in multi tag struct field")
}
}
func TestStructTagSeparator(t *testing.T) {
b := bytes.NewBufferString(`foo,BAR,Baz
e,3,b`)
d := newSimpleDecoderFromReader(b)
defaultTagSeparator := TagSeparator
TagSeparator = "|"
defer func() {
TagSeparator = defaultTagSeparator
}()
var samples []TagSeparatorSample
if err := readTo(d, &samples); err != nil {
t.Fatal(err)
}
if samples[0].Foo != "b" {
t.Fatal("expected second tag value in multi tag struct field.")
}
}
func TestCustomTag(t *testing.T) {
b := bytes.NewBufferString(`foo,BAR
e,3`)
d := newSimpleDecoderFromReader(b)
defaultTagName := TagName
TagName = "custom"
defer func() {
TagName = defaultTagName
}()
var samples []CustomTagSample
if err := readTo(d, &samples); err != nil {
t.Fatal(err)
}
if samples[0].Foo != "e" {
t.Fatal("wrong value in custom tag struct field")
}
}
func TestCSVToMap(t *testing.T) {
b := bytes.NewBufferString(`foo,BAR
4,Jose
2,Daniel
5,Vincent`)
m, err := CSVToMap(bytes.NewReader(b.Bytes()))
if err != nil {
t.Fatal(err)
}
if m["4"] != "Jose" {
t.Fatal("Expected Jose got", m["4"])
}
if m["2"] != "Daniel" {
t.Fatal("Expected Daniel got", m["2"])
}
if m["5"] != "Vincent" {
t.Fatal("Expected Vincent got", m["5"])
}
b = bytes.NewBufferString(`foo,BAR,Baz
e,3,b`)
_, err = CSVToMap(bytes.NewReader(b.Bytes()))
if err == nil {
t.Fatal("Something went wrong")
}
b = bytes.NewBufferString(`foo
e`)
_, err = CSVToMap(bytes.NewReader(b.Bytes()))
if err == nil {
t.Fatal("Something went wrong")
}
}
func TestCSVToMaps(t *testing.T) {
defer SetCSVReader(DefaultCSVReader)
type item struct {
setupReader func()
csv *bytes.Buffer
}
data := []item{
{
setupReader: func() {},
csv: bytes.NewBufferString(`foo,BAR,Baz
4,Jose,42
2,Daniel,21
5,Vincent,84`,
),
},
{
setupReader: func() {
SetCSVReader(func(in io.Reader) CSVReader {
r := csv.NewReader(in)
_, err := r.Read()
if err != nil {
t.Fatal(err)
}
return r
})
},
csv: bytes.NewBufferString((`,,
foo,BAR,Baz
4,Jose,42
2,Daniel,21
5,Vincent,84`),
),
},
}
for _, b := range data {
b.setupReader()
m, err := CSVToMaps(bytes.NewReader(b.csv.Bytes()))
if err != nil {
t.Fatal(err)
}
firstRecord := m[0]
if firstRecord["foo"] != "4" {
t.Fatal("Expected 4 got", firstRecord["foo"])
}
if firstRecord["BAR"] != "Jose" {
t.Fatal("Expected Jose got", firstRecord["BAR"])
}
if firstRecord["Baz"] != "42" {
t.Fatal("Expected 42 got", firstRecord["Baz"])
}
secondRecord := m[1]
if secondRecord["foo"] != "2" {
t.Fatal("Expected 2 got", secondRecord["foo"])
}
if secondRecord["BAR"] != "Daniel" {
t.Fatal("Expected Daniel got", secondRecord["BAR"])
}
if secondRecord["Baz"] != "21" {
t.Fatal("Expected 21 got", secondRecord["Baz"])
}
thirdRecord := m[2]
if thirdRecord["foo"] != "5" {
t.Fatal("Expected 5 got", thirdRecord["foo"])
}
if thirdRecord["BAR"] != "Vincent" {
t.Fatal("Expected Vincent got", thirdRecord["BAR"])
}
if thirdRecord["Baz"] != "84" {
t.Fatal("Expected 84 got", thirdRecord["Baz"])
}
}
}
type trimDecoder struct {
csvReader CSVReader
}
func (c *trimDecoder) GetCSVRow() ([]string, error) {
recoder, err := c.csvReader.Read()
for i, r := range recoder {
recoder[i] = strings.TrimRight(r, " ")
}
return recoder, err
}
func (c *trimDecoder) GetCSVRows() ([][]string, error) {
records := [][]string{}
for {
record, err := c.GetCSVRow()
if err == io.EOF {
break
} else if err != nil {
return nil, err
}
records = append(records, record)
}
return records, nil
}
func TestUnmarshalToDecoder(t *testing.T) {
blah := 0
sptr := "*string"
sptr2 := ""
b := bytes.NewBufferString(`foo,BAR,Baz,Blah,SPtr
f,1,baz,, *string
e,3,b,, `)
var samples []Sample
if err := UnmarshalDecoderToCallback(&trimDecoder{LazyCSVReader(b)}, func(s Sample) {
samples = append(samples, s)
}); err != nil {
t.Fatal(err)
}
if len(samples) != 2 {
t.Fatalf("expected 2 sample instances, got %d", len(samples))
}
expected := Sample{Foo: "f", Bar: 1, Baz: "baz", Blah: &blah, SPtr: &sptr}
if !reflect.DeepEqual(expected, samples[0]) {
t.Fatalf("expected first sample %v, got %v", expected, samples[0])
}
expected = Sample{Foo: "e", Bar: 3, Baz: "b", Blah: &blah, SPtr: &sptr2}
if !reflect.DeepEqual(expected, samples[1]) {
t.Fatalf("expected second sample %v, got %v", expected, samples[1])
}
}
func TestUnmarshalWithoutHeader(t *testing.T) {
blah := 0
sptr := ""
b := bytes.NewBufferString(`f,1,baz,1.66,,,
e,3,b,,,,`)
d := newSimpleDecoderFromReader(b)
var samples []Sample
if err := readToWithoutHeaders(d, &samples); err != nil {
t.Fatal(err)
}
expected := Sample{Foo: "f", Bar: 1, Baz: "baz", Frop: 1.66, Blah: &blah, SPtr: &sptr}
if !reflect.DeepEqual(expected, samples[0]) {
t.Fatalf("expected first sample %v, got %v", expected, samples[0])
}
expected = Sample{Foo: "e", Bar: 3, Baz: "b", Frop: 0, Blah: &blah, SPtr: &sptr}
if !reflect.DeepEqual(expected, samples[1]) {
t.Fatalf("expected second sample %v, got %v", expected, samples[1])
}
}
func TestUnmarshalCSVWithoutHeaders(t *testing.T) {
// tsv input to test custom csv reader
b := []byte("f\t1\tbaz\ne\t3\tblorp")
r := bytes.NewReader(b)
csvReader := csv.NewReader(r)
csvReader.Comma = '\t'
var samples []Sample
if err := UnmarshalCSVWithoutHeaders(csvReader, &samples); err != nil {
t.Fatal(err)
}
expected := Sample{Foo: "f", Bar: 1, Baz: "baz"}
if !reflect.DeepEqual(expected, samples[0]) {
t.Fatalf("expected first sample %v, got %v", expected, samples[0])
}
expected = Sample{Foo: "e", Bar: 3, Baz: "blorp"}
if !reflect.DeepEqual(expected, samples[1]) {
t.Fatalf("expected second sample %v, got %v", expected, samples[1])
}
}
func TestDecodeDefaultValues(t *testing.T) {
type defaultValueStruct struct {
Foo string `csv:"foo,default=x"`
Bar int `csv:"bar,default=42"`
}
b := bytes.NewBufferString(`foo,bar
,
`)
var out []defaultValueStruct
if err := Unmarshal(b, &out); err != nil {
t.Fatal(err)
}
expected := defaultValueStruct{
Foo: "x",
Bar: 42,
}
if !reflect.DeepEqual(expected, out[0]) {
t.Fatalf("expected second sample %v, got %v", expected, out[0])
}
}
func TestDecodePartialKeys(t *testing.T) {
type defaultValueStruct struct {
Foo string `csv:"foo,partial"`
Bar int `csv:"bar,partial"`
}
b := bytes.NewBufferString(`foolo,ambar
x,42
`)
var out []defaultValueStruct
if err := Unmarshal(b, &out); err != nil {
t.Fatal(err)
}
expected := defaultValueStruct{
Foo: "x",
Bar: 42,
}
if !reflect.DeepEqual(expected, out[0]) {
t.Fatalf("expected second sample %v, got %v", expected, out[0])
}
}
func TestTrimTagWhitespace(t *testing.T) {
type whiteSpaceOptionStruct struct {
Foo *string `csv:"foo, omitempty"`
Bar int `csv:"bar, default=13 "`
}
var out []whiteSpaceOptionStruct
b := bytes.NewBufferString(`foo,bar
,`)
if err := Unmarshal(b, &out); err != nil {
t.Fatal(err)
}
expected := whiteSpaceOptionStruct{
Foo: nil,
Bar: 13,
}
if !reflect.DeepEqual(expected, out[0]) {
t.Fatalf("expected sample %v, got %v", expected, out[0])
}
}
func TestUnmarshalCSVToMap(t *testing.T) {
b := []byte(`line tokens
10 ["PRINT", "\"Hello map!\""]
20 ["GOTO", "10"]`)
r := bytes.NewReader(b)
csvReader := csv.NewReader(r)
csvReader.LazyQuotes = true
csvReader.Comma = '\t'
var sample map[int][]string
if err := UnmarshalCSVToMap(csvReader, &sample); err != nil {
t.Fatal(err)
}
expected := map[int][]string{
10: {"PRINT", "\"Hello map!\""},
20: {"GOTO", "10"},
}
if !reflect.DeepEqual(expected, sample) {
t.Fatalf("expected %v, got %v", expected, sample)
}
}
func BenchmarkCSVToMap(b *testing.B) {
bufstring := bytes.NewBufferString(`foo,BAR
4,Jose
2,Daniel
5,Vincent`)
for n := 0; n < b.N; n++ {
_, err := CSVToMap(bytes.NewReader(bufstring.Bytes()))
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkUnmarshalCSVToMap(b *testing.B) {
bufstring := []byte(`foo,BAR
4,Jose
2,Daniel
5,Vincent`)
for n := 0; n < b.N; n++ {
var sample map[string]string
r := bytes.NewReader(bufstring)
d := csv.NewReader(r)
err := UnmarshalCSVToMap(d, &sample)
if err != nil {
b.Fatal(err)
}
}
}
func Test_readTo_nested_struct(t *testing.T) {
b := bytes.NewBufferString(`one.boolField1,one.stringField2,two.boolField1,two.stringField2,three.boolField1,three.stringField2
false,email_one,true,email_two,false,email_three`)
d := newSimpleDecoderFromReader(b)
var samples []NestedSample
err := readTo(d, &samples)
if err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
expected := []NestedSample{
{
Inner1: InnerStruct{
BoolIgnoreField0: false,
BoolField1: false,
StringField2: "email_one",
},
Inner2: InnerStruct{
BoolIgnoreField0: false,
BoolField1: true,
StringField2: "email_two",
},
Inner3: NestedEmbedSample{InnerStruct{
BoolIgnoreField0: false,
BoolField1: false,
StringField2: "email_three",
}},
},
}
if !reflect.DeepEqual(expected, samples) {
t.Fatalf("expected \n sample: %v\n got: %v", expected, samples)
}
}
func Test_readTo_inline_nested_struct(t *testing.T) {
b := bytes.NewBufferString("a,b,x\n1,2,3")
d := newSimpleDecoderFromReader(b)
var samples []InlineFooSample
if err := readTo(d, &samples); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
expected := []InlineFooSample{{Bar: InlineBar{A: 1, B: 2}, X: 3}}
if !reflect.DeepEqual(expected, samples) {
t.Fatalf("expected \n sample: %v\n got: %v", expected, samples)
}
}
func TestUnmarshalZeroPaddedNumbers(t *testing.T) {
type row struct {
Name string `csv:"name"`
Code int `csv:"code"`
Count uint `csv:"count"`
}
in := "name,code,count\n" +
"a,08,08\n" +
"b,09,09\n" +
"c,010,010\n" +
"d,007,007\n"
var got []row
if err := UnmarshalString(in, &got); err != nil {
t.Fatalf("UnmarshalString failed on zero-padded numbers: %s", err)
}
want := []row{
{Name: "a", Code: 8, Count: 8},
{Name: "b", Code: 9, Count: 9},
{Name: "c", Code: 10, Count: 10},
{Name: "d", Code: 7, Count: 7},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("UnmarshalString returned %+v, want %+v", got, want)
}
}