1// Copyright 2019 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package csv
6
7import (
8	"bytes"
9	"reflect"
10	"slices"
11	"strings"
12	"testing"
13)
14
15func FuzzRoundtrip(f *testing.F) {
16	f.Fuzz(func(t *testing.T, in []byte) {
17		buf := new(bytes.Buffer)
18
19		t.Logf("input = %q", in)
20		for _, tt := range []Reader{
21			{Comma: ','},
22			{Comma: ';'},
23			{Comma: '\t'},
24			{Comma: ',', LazyQuotes: true},
25			{Comma: ',', TrimLeadingSpace: true},
26			{Comma: ',', Comment: '#'},
27			{Comma: ',', Comment: ';'},
28		} {
29			t.Logf("With options:")
30			t.Logf("  Comma            = %q", tt.Comma)
31			t.Logf("  LazyQuotes       = %t", tt.LazyQuotes)
32			t.Logf("  TrimLeadingSpace = %t", tt.TrimLeadingSpace)
33			t.Logf("  Comment          = %q", tt.Comment)
34			r := NewReader(bytes.NewReader(in))
35			r.Comma = tt.Comma
36			r.Comment = tt.Comment
37			r.LazyQuotes = tt.LazyQuotes
38			r.TrimLeadingSpace = tt.TrimLeadingSpace
39
40			records, err := r.ReadAll()
41			if err != nil {
42				continue
43			}
44			t.Logf("first records = %#v", records)
45
46			buf.Reset()
47			w := NewWriter(buf)
48			w.Comma = tt.Comma
49			err = w.WriteAll(records)
50			if err != nil {
51				t.Logf("writer  = %#v\n", w)
52				t.Logf("records = %v\n", records)
53				t.Fatal(err)
54			}
55			if tt.Comment != 0 {
56				// Writer doesn't support comments, so it can turn the quoted record "#"
57				// into a non-quoted comment line, failing the roundtrip check below.
58				continue
59			}
60			t.Logf("second input = %q", buf.Bytes())
61
62			r = NewReader(buf)
63			r.Comma = tt.Comma
64			r.Comment = tt.Comment
65			r.LazyQuotes = tt.LazyQuotes
66			r.TrimLeadingSpace = tt.TrimLeadingSpace
67			result, err := r.ReadAll()
68			if err != nil {
69				t.Logf("reader  = %#v\n", r)
70				t.Logf("records = %v\n", records)
71				t.Fatal(err)
72			}
73
74			// The reader turns \r\n into \n.
75			for _, record := range records {
76				for i, s := range record {
77					record[i] = strings.ReplaceAll(s, "\r\n", "\n")
78				}
79			}
80			// Note that the reader parses the quoted record "" as an empty string,
81			// and the writer turns that into an empty line, which the reader skips over.
82			// Filter those out to avoid false positives.
83			records = slices.DeleteFunc(records, func(record []string) bool {
84				return len(record) == 1 && record[0] == ""
85			})
86			// The reader uses nil when returning no records at all.
87			if len(records) == 0 {
88				records = nil
89			}
90
91			if !reflect.DeepEqual(records, result) {
92				t.Fatalf("first read got %#v, second got %#v", records, result)
93			}
94		}
95	})
96}
97