Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
509 lines (490 sloc)
14.2 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Copyright 2011 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package csv | |
| import ( | |
| "io" | |
| "reflect" | |
| "strings" | |
| "testing" | |
| "unicode/utf8" | |
| ) | |
| func TestRead(t *testing.T) { | |
| tests := []struct { | |
| Name string | |
| Input string | |
| Output [][]string | |
| Error error | |
| // These fields are copied into the Reader | |
| Comma rune | |
| Comment rune | |
| UseFieldsPerRecord bool // false (default) means FieldsPerRecord is -1 | |
| FieldsPerRecord int | |
| LazyQuotes bool | |
| TrimLeadingSpace bool | |
| ReuseRecord bool | |
| }{{ | |
| Name: "Simple", | |
| Input: "a,b,c\n", | |
| Output: [][]string{{"a", "b", "c"}}, | |
| }, { | |
| Name: "CRLF", | |
| Input: "a,b\r\nc,d\r\n", | |
| Output: [][]string{{"a", "b"}, {"c", "d"}}, | |
| }, { | |
| Name: "BareCR", | |
| Input: "a,b\rc,d\r\n", | |
| Output: [][]string{{"a", "b\rc", "d"}}, | |
| }, { | |
| Name: "RFC4180test", | |
| Input: `#field1,field2,field3 | |
| "aaa","bb | |
| b","ccc" | |
| "a,a","b""bb","ccc" | |
| zzz,yyy,xxx | |
| `, | |
| Output: [][]string{ | |
| {"#field1", "field2", "field3"}, | |
| {"aaa", "bb\nb", "ccc"}, | |
| {"a,a", `b"bb`, "ccc"}, | |
| {"zzz", "yyy", "xxx"}, | |
| }, | |
| UseFieldsPerRecord: true, | |
| FieldsPerRecord: 0, | |
| }, { | |
| Name: "NoEOLTest", | |
| Input: "a,b,c", | |
| Output: [][]string{{"a", "b", "c"}}, | |
| }, { | |
| Name: "Semicolon", | |
| Input: "a;b;c\n", | |
| Output: [][]string{{"a", "b", "c"}}, | |
| Comma: ';', | |
| }, { | |
| Name: "MultiLine", | |
| Input: `"two | |
| line","one line","three | |
| line | |
| field"`, | |
| Output: [][]string{{"two\nline", "one line", "three\nline\nfield"}}, | |
| }, { | |
| Name: "BlankLine", | |
| Input: "a,b,c\n\nd,e,f\n\n", | |
| Output: [][]string{ | |
| {"a", "b", "c"}, | |
| {"d", "e", "f"}, | |
| }, | |
| }, { | |
| Name: "BlankLineFieldCount", | |
| Input: "a,b,c\n\nd,e,f\n\n", | |
| Output: [][]string{ | |
| {"a", "b", "c"}, | |
| {"d", "e", "f"}, | |
| }, | |
| UseFieldsPerRecord: true, | |
| FieldsPerRecord: 0, | |
| }, { | |
| Name: "TrimSpace", | |
| Input: " a, b, c\n", | |
| Output: [][]string{{"a", "b", "c"}}, | |
| TrimLeadingSpace: true, | |
| }, { | |
| Name: "LeadingSpace", | |
| Input: " a, b, c\n", | |
| Output: [][]string{{" a", " b", " c"}}, | |
| }, { | |
| Name: "Comment", | |
| Input: "#1,2,3\na,b,c\n#comment", | |
| Output: [][]string{{"a", "b", "c"}}, | |
| Comment: '#', | |
| }, { | |
| Name: "NoComment", | |
| Input: "#1,2,3\na,b,c", | |
| Output: [][]string{{"#1", "2", "3"}, {"a", "b", "c"}}, | |
| }, { | |
| Name: "LazyQuotes", | |
| Input: `a "word","1"2",a","b`, | |
| Output: [][]string{{`a "word"`, `1"2`, `a"`, `b`}}, | |
| LazyQuotes: true, | |
| }, { | |
| Name: "BareQuotes", | |
| Input: `a "word","1"2",a"`, | |
| Output: [][]string{{`a "word"`, `1"2`, `a"`}}, | |
| LazyQuotes: true, | |
| }, { | |
| Name: "BareDoubleQuotes", | |
| Input: `a""b,c`, | |
| Output: [][]string{{`a""b`, `c`}}, | |
| LazyQuotes: true, | |
| }, { | |
| Name: "BadDoubleQuotes", | |
| Input: `a""b,c`, | |
| Error: &ParseError{StartLine: 1, Line: 1, Column: 1, Err: ErrBareQuote}, | |
| }, { | |
| Name: "TrimQuote", | |
| Input: ` "a"," b",c`, | |
| Output: [][]string{{"a", " b", "c"}}, | |
| TrimLeadingSpace: true, | |
| }, { | |
| Name: "BadBareQuote", | |
| Input: `a "word","b"`, | |
| Error: &ParseError{StartLine: 1, Line: 1, Column: 2, Err: ErrBareQuote}, | |
| }, { | |
| Name: "BadTrailingQuote", | |
| Input: `"a word",b"`, | |
| Error: &ParseError{StartLine: 1, Line: 1, Column: 10, Err: ErrBareQuote}, | |
| }, { | |
| Name: "ExtraneousQuote", | |
| Input: `"a "word","b"`, | |
| Error: &ParseError{StartLine: 1, Line: 1, Column: 3, Err: ErrQuote}, | |
| }, { | |
| Name: "BadFieldCount", | |
| Input: "a,b,c\nd,e", | |
| Error: &ParseError{StartLine: 2, Line: 2, Err: ErrFieldCount}, | |
| UseFieldsPerRecord: true, | |
| FieldsPerRecord: 0, | |
| }, { | |
| Name: "BadFieldCount1", | |
| Input: `a,b,c`, | |
| Error: &ParseError{StartLine: 1, Line: 1, Err: ErrFieldCount}, | |
| UseFieldsPerRecord: true, | |
| FieldsPerRecord: 2, | |
| }, { | |
| Name: "FieldCount", | |
| Input: "a,b,c\nd,e", | |
| Output: [][]string{{"a", "b", "c"}, {"d", "e"}}, | |
| }, { | |
| Name: "TrailingCommaEOF", | |
| Input: "a,b,c,", | |
| Output: [][]string{{"a", "b", "c", ""}}, | |
| }, { | |
| Name: "TrailingCommaEOL", | |
| Input: "a,b,c,\n", | |
| Output: [][]string{{"a", "b", "c", ""}}, | |
| }, { | |
| Name: "TrailingCommaSpaceEOF", | |
| Input: "a,b,c, ", | |
| Output: [][]string{{"a", "b", "c", ""}}, | |
| TrimLeadingSpace: true, | |
| }, { | |
| Name: "TrailingCommaSpaceEOL", | |
| Input: "a,b,c, \n", | |
| Output: [][]string{{"a", "b", "c", ""}}, | |
| TrimLeadingSpace: true, | |
| }, { | |
| Name: "TrailingCommaLine3", | |
| Input: "a,b,c\nd,e,f\ng,hi,", | |
| Output: [][]string{{"a", "b", "c"}, {"d", "e", "f"}, {"g", "hi", ""}}, | |
| TrimLeadingSpace: true, | |
| }, { | |
| Name: "NotTrailingComma3", | |
| Input: "a,b,c, \n", | |
| Output: [][]string{{"a", "b", "c", " "}}, | |
| }, { | |
| Name: "CommaFieldTest", | |
| Input: `x,y,z,w | |
| x,y,z, | |
| x,y,, | |
| x,,, | |
| ,,, | |
| "x","y","z","w" | |
| "x","y","z","" | |
| "x","y","","" | |
| "x","","","" | |
| "","","","" | |
| `, | |
| Output: [][]string{ | |
| {"x", "y", "z", "w"}, | |
| {"x", "y", "z", ""}, | |
| {"x", "y", "", ""}, | |
| {"x", "", "", ""}, | |
| {"", "", "", ""}, | |
| {"x", "y", "z", "w"}, | |
| {"x", "y", "z", ""}, | |
| {"x", "y", "", ""}, | |
| {"x", "", "", ""}, | |
| {"", "", "", ""}, | |
| }, | |
| }, { | |
| Name: "TrailingCommaIneffective1", | |
| Input: "a,b,\nc,d,e", | |
| Output: [][]string{ | |
| {"a", "b", ""}, | |
| {"c", "d", "e"}, | |
| }, | |
| TrimLeadingSpace: true, | |
| }, { | |
| Name: "ReadAllReuseRecord", | |
| Input: "a,b\nc,d", | |
| Output: [][]string{ | |
| {"a", "b"}, | |
| {"c", "d"}, | |
| }, | |
| ReuseRecord: true, | |
| }, { | |
| Name: "StartLine1", // Issue 19019 | |
| Input: "a,\"b\nc\"d,e", | |
| Error: &ParseError{StartLine: 1, Line: 2, Column: 1, Err: ErrQuote}, | |
| }, { | |
| Name: "StartLine2", | |
| Input: "a,b\n\"d\n\n,e", | |
| Error: &ParseError{StartLine: 2, Line: 5, Column: 0, Err: ErrQuote}, | |
| }, { | |
| Name: "CRLFInQuotedField", // Issue 21201 | |
| Input: "A,\"Hello\r\nHi\",B\r\n", | |
| Output: [][]string{ | |
| {"A", "Hello\nHi", "B"}, | |
| }, | |
| }, { | |
| Name: "BinaryBlobField", // Issue 19410 | |
| Input: "x09\x41\xb4\x1c,aktau", | |
| Output: [][]string{{"x09A\xb4\x1c", "aktau"}}, | |
| }, { | |
| Name: "TrailingCR", | |
| Input: "field1,field2\r", | |
| Output: [][]string{{"field1", "field2"}}, | |
| }, { | |
| Name: "QuotedTrailingCR", | |
| Input: "\"field\"\r", | |
| Output: [][]string{{"field"}}, | |
| }, { | |
| Name: "QuotedTrailingCRCR", | |
| Input: "\"field\"\r\r", | |
| Error: &ParseError{StartLine: 1, Line: 1, Column: 6, Err: ErrQuote}, | |
| }, { | |
| Name: "FieldCR", | |
| Input: "field\rfield\r", | |
| Output: [][]string{{"field\rfield"}}, | |
| }, { | |
| Name: "FieldCRCR", | |
| Input: "field\r\rfield\r\r", | |
| Output: [][]string{{"field\r\rfield\r"}}, | |
| }, { | |
| Name: "FieldCRCRLF", | |
| Input: "field\r\r\nfield\r\r\n", | |
| Output: [][]string{{"field\r"}, {"field\r"}}, | |
| }, { | |
| Name: "FieldCRCRLFCR", | |
| Input: "field\r\r\n\rfield\r\r\n\r", | |
| Output: [][]string{{"field\r"}, {"\rfield\r"}}, | |
| }, { | |
| Name: "FieldCRCRLFCRCR", | |
| Input: "field\r\r\n\r\rfield\r\r\n\r\r", | |
| Output: [][]string{{"field\r"}, {"\r\rfield\r"}, {"\r"}}, | |
| }, { | |
| Name: "MultiFieldCRCRLFCRCR", | |
| Input: "field1,field2\r\r\n\r\rfield1,field2\r\r\n\r\r,", | |
| Output: [][]string{ | |
| {"field1", "field2\r"}, | |
| {"\r\rfield1", "field2\r"}, | |
| {"\r\r", ""}, | |
| }, | |
| }, { | |
| Name: "NonASCIICommaAndComment", | |
| Input: "a£b,c£ \td,e\n€ comment\n", | |
| Output: [][]string{{"a", "b,c", "d,e"}}, | |
| TrimLeadingSpace: true, | |
| Comma: '£', | |
| Comment: '€', | |
| }, { | |
| Name: "NonASCIICommaAndCommentWithQuotes", | |
| Input: "a€\" b,\"€ c\nλ comment\n", | |
| Output: [][]string{{"a", " b,", " c"}}, | |
| Comma: '€', | |
| Comment: 'λ', | |
| }, { | |
| // λ and θ start with the same byte. | |
| // This tests that the parser doesn't confuse such characters. | |
| Name: "NonASCIICommaConfusion", | |
| Input: "\"abθcd\"λefθgh", | |
| Output: [][]string{{"abθcd", "efθgh"}}, | |
| Comma: 'λ', | |
| Comment: '€', | |
| }, { | |
| Name: "NonASCIICommentConfusion", | |
| Input: "λ\nλ\nθ\nλ\n", | |
| Output: [][]string{{"λ"}, {"λ"}, {"λ"}}, | |
| Comment: 'θ', | |
| }, { | |
| Name: "QuotedFieldMultipleLF", | |
| Input: "\"\n\n\n\n\"", | |
| Output: [][]string{{"\n\n\n\n"}}, | |
| }, { | |
| Name: "MultipleCRLF", | |
| Input: "\r\n\r\n\r\n\r\n", | |
| }, { | |
| // The implementation may read each line in several chunks if it doesn't fit entirely | |
| // in the read buffer, so we should test the code to handle that condition. | |
| Name: "HugeLines", | |
| Input: strings.Repeat("#ignore\n", 10000) + strings.Repeat("@", 5000) + "," + strings.Repeat("*", 5000), | |
| Output: [][]string{{strings.Repeat("@", 5000), strings.Repeat("*", 5000)}}, | |
| Comment: '#', | |
| }, { | |
| Name: "QuoteWithTrailingCRLF", | |
| Input: "\"foo\"bar\"\r\n", | |
| Error: &ParseError{StartLine: 1, Line: 1, Column: 4, Err: ErrQuote}, | |
| }, { | |
| Name: "LazyQuoteWithTrailingCRLF", | |
| Input: "\"foo\"bar\"\r\n", | |
| Output: [][]string{{`foo"bar`}}, | |
| LazyQuotes: true, | |
| }, { | |
| Name: "DoubleQuoteWithTrailingCRLF", | |
| Input: "\"foo\"\"bar\"\r\n", | |
| Output: [][]string{{`foo"bar`}}, | |
| }, { | |
| Name: "EvenQuotes", | |
| Input: `""""""""`, | |
| Output: [][]string{{`"""`}}, | |
| }, { | |
| Name: "OddQuotes", | |
| Input: `"""""""`, | |
| Error: &ParseError{StartLine: 1, Line: 1, Column: 7, Err: ErrQuote}, | |
| }, { | |
| Name: "LazyOddQuotes", | |
| Input: `"""""""`, | |
| Output: [][]string{{`"""`}}, | |
| LazyQuotes: true, | |
| }, { | |
| Name: "BadComma1", | |
| Comma: '\n', | |
| Error: errInvalidDelim, | |
| }, { | |
| Name: "BadComma2", | |
| Comma: '\r', | |
| Error: errInvalidDelim, | |
| }, { | |
| Name: "BadComma3", | |
| Comma: '"', | |
| Error: errInvalidDelim, | |
| }, { | |
| Name: "BadComma4", | |
| Comma: utf8.RuneError, | |
| Error: errInvalidDelim, | |
| }, { | |
| Name: "BadComment1", | |
| Comment: '\n', | |
| Error: errInvalidDelim, | |
| }, { | |
| Name: "BadComment2", | |
| Comment: '\r', | |
| Error: errInvalidDelim, | |
| }, { | |
| Name: "BadComment3", | |
| Comment: utf8.RuneError, | |
| Error: errInvalidDelim, | |
| }, { | |
| Name: "BadCommaComment", | |
| Comma: 'X', | |
| Comment: 'X', | |
| Error: errInvalidDelim, | |
| }} | |
| for _, tt := range tests { | |
| t.Run(tt.Name, func(t *testing.T) { | |
| r := NewReader(strings.NewReader(tt.Input)) | |
| if tt.Comma != 0 { | |
| r.Comma = tt.Comma | |
| } | |
| r.Comment = tt.Comment | |
| if tt.UseFieldsPerRecord { | |
| r.FieldsPerRecord = tt.FieldsPerRecord | |
| } else { | |
| r.FieldsPerRecord = -1 | |
| } | |
| r.LazyQuotes = tt.LazyQuotes | |
| r.TrimLeadingSpace = tt.TrimLeadingSpace | |
| r.ReuseRecord = tt.ReuseRecord | |
| out, err := r.ReadAll() | |
| if !reflect.DeepEqual(err, tt.Error) { | |
| t.Errorf("ReadAll() error:\ngot %v\nwant %v", err, tt.Error) | |
| } else if !reflect.DeepEqual(out, tt.Output) { | |
| t.Errorf("ReadAll() output:\ngot %q\nwant %q", out, tt.Output) | |
| } | |
| }) | |
| } | |
| } | |
| // nTimes is an io.Reader which yields the string s n times. | |
| type nTimes struct { | |
| s string | |
| n int | |
| off int | |
| } | |
| func (r *nTimes) Read(p []byte) (n int, err error) { | |
| for { | |
| if r.n <= 0 || r.s == "" { | |
| return n, io.EOF | |
| } | |
| n0 := copy(p, r.s[r.off:]) | |
| p = p[n0:] | |
| n += n0 | |
| r.off += n0 | |
| if r.off == len(r.s) { | |
| r.off = 0 | |
| r.n-- | |
| } | |
| if len(p) == 0 { | |
| return | |
| } | |
| } | |
| } | |
| // benchmarkRead measures reading the provided CSV rows data. | |
| // initReader, if non-nil, modifies the Reader before it's used. | |
| func benchmarkRead(b *testing.B, initReader func(*Reader), rows string) { | |
| b.ReportAllocs() | |
| r := NewReader(&nTimes{s: rows, n: b.N}) | |
| if initReader != nil { | |
| initReader(r) | |
| } | |
| for { | |
| _, err := r.Read() | |
| if err == io.EOF { | |
| break | |
| } | |
| if err != nil { | |
| b.Fatal(err) | |
| } | |
| } | |
| } | |
| const benchmarkCSVData = `x,y,z,w | |
| x,y,z, | |
| x,y,, | |
| x,,, | |
| ,,, | |
| "x","y","z","w" | |
| "x","y","z","" | |
| "x","y","","" | |
| "x","","","" | |
| "","","","" | |
| ` | |
| func BenchmarkRead(b *testing.B) { | |
| benchmarkRead(b, nil, benchmarkCSVData) | |
| } | |
| func BenchmarkReadWithFieldsPerRecord(b *testing.B) { | |
| benchmarkRead(b, func(r *Reader) { r.FieldsPerRecord = 4 }, benchmarkCSVData) | |
| } | |
| func BenchmarkReadWithoutFieldsPerRecord(b *testing.B) { | |
| benchmarkRead(b, func(r *Reader) { r.FieldsPerRecord = -1 }, benchmarkCSVData) | |
| } | |
| func BenchmarkReadLargeFields(b *testing.B) { | |
| benchmarkRead(b, nil, strings.Repeat(`xxxxxxxxxxxxxxxx,yyyyyyyyyyyyyyyy,zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv | |
| xxxxxxxxxxxxxxxxxxxxxxxx,yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy,zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvv | |
| ,,zzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv | |
| xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy,zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv | |
| `, 3)) | |
| } | |
| func BenchmarkReadReuseRecord(b *testing.B) { | |
| benchmarkRead(b, func(r *Reader) { r.ReuseRecord = true }, benchmarkCSVData) | |
| } | |
| func BenchmarkReadReuseRecordWithFieldsPerRecord(b *testing.B) { | |
| benchmarkRead(b, func(r *Reader) { r.ReuseRecord = true; r.FieldsPerRecord = 4 }, benchmarkCSVData) | |
| } | |
| func BenchmarkReadReuseRecordWithoutFieldsPerRecord(b *testing.B) { | |
| benchmarkRead(b, func(r *Reader) { r.ReuseRecord = true; r.FieldsPerRecord = -1 }, benchmarkCSVData) | |
| } | |
| func BenchmarkReadReuseRecordLargeFields(b *testing.B) { | |
| benchmarkRead(b, func(r *Reader) { r.ReuseRecord = true }, strings.Repeat(`xxxxxxxxxxxxxxxx,yyyyyyyyyyyyyyyy,zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv | |
| xxxxxxxxxxxxxxxxxxxxxxxx,yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy,zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvv | |
| ,,zzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv | |
| xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy,zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv | |
| `, 3)) | |
| } |