diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ae6c6e1..177cc10c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ ## Changelog +### [13.12.0](https://kaos.sh/ek/13.12.0) + +* `[req]` Added custom timeout per request +* `[req]` Added `Retrier` +* `[req]` Make `Limiter` public +* `[log]` Added `WithFullCallerPath` option to enable the output of the full caller path +* `[strutil]` Added support of escaped strings to `Fields` +* `[strutil]` Added fuzz tests for `Fields` method +* `[knf]` Fixed build of fuzz tests + ### [13.11.0](https://kaos.sh/ek/13.11.0) * `[req]` Added request limiter diff --git a/Makefile b/Makefile index 839caa77..87582a93 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,6 @@ ifdef VERBOSE ## Print verbose information (Flag) VERBOSE_FLAG = -v endif -COMPAT ?= 1.19 MAKEDIR = $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) GITREV ?= $(shell test -s $(MAKEDIR)/.git && git rev-parse --short HEAD) diff --git a/knf/fuzz.go b/knf/fuzz.go index 36cbeae1..0741cc70 100644 --- a/knf/fuzz.go +++ b/knf/fuzz.go @@ -17,7 +17,7 @@ import ( // ////////////////////////////////////////////////////////////////////////////////// // func Fuzz(data []byte) int { - _, err := readKNFData(bytes.NewReader(data)) + _, err := readData(bytes.NewReader(data)) if err != nil { return 0 diff --git a/log/log.go b/log/log.go index b1bb1d08..a381233b 100644 --- a/log/log.go +++ b/log/log.go @@ -64,10 +64,11 @@ type Logger struct { PrefixError bool // Show prefix for error messages PrefixCrit bool // Show prefix for critical/fatal messages - TimeLayout string // Date and time layout used for rendering dates - UseColors bool // Enable ANSI escape codes for colors in output - UseJSON bool // Encode messages to JSON - WithCaller bool // Show caller info + TimeLayout string // Date and time layout used for rendering dates + UseColors bool // Enable ANSI escape codes for colors in output + UseJSON bool // Encode messages to JSON + WithCaller bool // Show caller info + WithFullCallerPath bool // Show full path of caller file string buf bytes.Buffer @@ -517,9 +518,9 @@ func (l *Logger) writeText(level uint8, f string, a ...any) error { if l.WithCaller { if l.UseColors { - fmtc.Fprintf(&l.buf, "{s-}(%s){!} ", getCallerFromStack()) + fmtc.Fprintf(&l.buf, "{s-}(%s){!} ", getCallerFromStack(l.WithFullCallerPath)) } else { - l.buf.WriteString("(" + getCallerFromStack() + ") ") + l.buf.WriteString("(" + getCallerFromStack(l.WithFullCallerPath) + ") ") } } @@ -577,7 +578,7 @@ func (l *Logger) writeJSON(level uint8, msg string, a ...any) error { l.writeJSONTimestamp() if l.WithCaller { - l.buf.WriteString(`"caller":"` + getCallerFromStack() + `",`) + l.buf.WriteString(`"caller":"` + getCallerFromStack(l.WithFullCallerPath) + `",`) } operands, fields := splitPayload(a) @@ -880,7 +881,7 @@ func splitPayload(payload []any) ([]any, []any) { } // getCallerFromStack returns caller function and line from stack -func getCallerFromStack() string { +func getCallerFromStack(full bool) string { pcs := make([]uintptr, 64) n := runtime.Callers(2, pcs) @@ -902,14 +903,18 @@ func getCallerFromStack() string { continue } - return extractCallerFromFrame(frame) + return extractCallerFromFrame(frame, full) } return "unknown" } // extractCallerFromFrame extracts caller info from frame -func extractCallerFromFrame(f runtime.Frame) string { +func extractCallerFromFrame(f runtime.Frame, full bool) string { + if full { + return f.File + ":" + strconv.Itoa(f.Line) + } + index := strutil.IndexByteSkip(f.File, '/', -1) return f.File[index+1:] + ":" + strconv.Itoa(f.Line) } diff --git a/log/log_test.go b/log/log_test.go index 0759907d..89dab4a2 100644 --- a/log/log_test.go +++ b/log/log_test.go @@ -10,6 +10,7 @@ package log import ( "encoding/json" "os" + "runtime" "strings" "sync" "testing" @@ -485,8 +486,12 @@ func (ls *LogSuite) TestWithCaller(c *C) { c.Assert(len(dataSlice), Equals, 3) - c.Assert(dataSlice[0][28:], Equals, "(log/log_test.go:470) Test info 1") - c.Assert(dataSlice[1][28:], Equals, "(log/log_test.go:475) Test info 2") + c.Assert(dataSlice[0][28:], Equals, "(log/log_test.go:471) Test info 1") + c.Assert(dataSlice[1][28:], Equals, "(log/log_test.go:476) Test info 2") + + frm := runtime.Frame{File: "/path/to/my/app/code/test.go", Line: 10} + c.Assert(extractCallerFromFrame(frm, true), Equals, "/path/to/my/app/code/test.go:10") + c.Assert(extractCallerFromFrame(frm, false), Equals, "code/test.go:10") } func (ls *LogSuite) TestWithFields(c *C) { diff --git a/req/example_test.go b/req/example_test.go index 1721ed33..f3e0e963 100644 --- a/req/example_test.go +++ b/req/example_test.go @@ -9,6 +9,7 @@ package req import ( "fmt" + "time" ) // ////////////////////////////////////////////////////////////////////////////////// // @@ -153,3 +154,20 @@ func ExampleRequest_PostFile() { fmt.Println("File successfully uploaded!") } + +func ExampleNewRetrier() { + r := NewRetrier() + + resp, err := r.Get( + Request{URL: "https://my.domain.com"}, + Retry{Num: 5, Status: STATUS_OK, Pause: time.Second}, + ) + + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + + // print status code + fmt.Printf("Status code: %d\n", resp.StatusCode) +} diff --git a/req/limiter.go b/req/limiter.go index 210a8142..41f998fc 100644 --- a/req/limiter.go +++ b/req/limiter.go @@ -11,21 +11,21 @@ import "time" // ////////////////////////////////////////////////////////////////////////////////// // -// limiter is request limiter -type limiter struct { +// Limiter is request limiter +type Limiter struct { lastCall time.Time delay time.Duration } // ////////////////////////////////////////////////////////////////////////////////// // -// createLimiter creates new limiter -func createLimiter(rps float64) *limiter { +// NewLimiter creates a new limiter. If rps is less than or equal to 0, it returns nil. +func NewLimiter(rps float64) *Limiter { if rps <= 0 { return nil } - return &limiter{ + return &Limiter{ delay: time.Duration(float64(time.Second) / rps), } } @@ -33,7 +33,7 @@ func createLimiter(rps float64) *limiter { // ////////////////////////////////////////////////////////////////////////////////// // // Wait blocks current goroutine execution until next time slot become available -func (l *limiter) Wait() { +func (l *Limiter) Wait() { if l == nil { return } diff --git a/req/req.go b/req/req.go index b1168d1a..b1a6c394 100644 --- a/req/req.go +++ b/req/req.go @@ -10,6 +10,7 @@ package req import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -189,19 +190,20 @@ type Headers map[string]string // Request is basic struct type Request struct { - Method string // Request method - URL string // Request URL - Query Query // Map with query params - Body any // Request body - Headers Headers // Map with headers - ContentType string // Content type header - Accept string // Accept header - BasicAuthUsername string // Basic auth username - BasicAuthPassword string // Basic auth password - BearerAuth string // Bearer auth token - AutoDiscard bool // Automatically discard all responses with status code > 299 - FollowRedirect bool // Follow redirect - Close bool // Close indicates whether to close the connection after sending request + Method string // Request method + URL string // Request URL + Query Query // Map with query params + Body any // Request body + Headers Headers // Map with headers + ContentType string // Content type header + Accept string // Accept header + BasicAuthUsername string // Basic auth username + BasicAuthPassword string // Basic auth password + BearerAuth string // Bearer auth token + Timeout time.Duration // Request timeout + AutoDiscard bool // Automatically discard all responses with status code > 299 + FollowRedirect bool // Follow redirect + Close bool // Close indicates whether to close the connection after sending request } // Response is struct contains response data and properties @@ -224,7 +226,7 @@ type Engine struct { Transport *http.Transport // Transport is default transport struct Client *http.Client // Client is default client struct - limiter *limiter // Request limiter + limiter *Limiter // Request limiter dialTimeout float64 // dialTimeout is dial timeout in seconds requestTimeout float64 // requestTimeout is request timeout in seconds @@ -367,6 +369,11 @@ func (e *Engine) Patch(r Request) (*Response, error) { return e.doRequest(r, PATCH) } +// Delete sends DELETE request and process response +func (e *Engine) Delete(r Request) (*Response, error) { + return e.doRequest(r, DELETE) +} + // PostFile sends multipart POST request with file data func (e *Engine) PostFile(r Request, file, fieldName string, extraFields map[string]string) (*Response, error) { err := configureMultipartRequest(&r, file, fieldName, extraFields) @@ -378,11 +385,6 @@ func (e *Engine) PostFile(r Request, file, fieldName string, extraFields map[str return e.doRequest(r, POST) } -// Delete sends DELETE request and process response -func (e *Engine) Delete(r Request) (*Response, error) { - return e.doRequest(r, DELETE) -} - // SetUserAgent sets user agent based on app name and version func (e *Engine) SetUserAgent(app, version string, subs ...string) { if e == nil { @@ -437,7 +439,7 @@ func (e *Engine) SetLimit(rps float64) { return } - e.limiter = createLimiter(rps) + e.limiter = NewLimiter(rps) } // ////////////////////////////////////////////////////////////////////////////////// // @@ -617,7 +619,11 @@ func (e *Engine) doRequest(r Request, method string) (*Response, error) { r.ContentType = contentType } - req, err := createRequest(e, r, bodyReader) + req, cancel, err := createRequest(e, r, bodyReader) + + if cancel != nil { + defer cancel() + } if err != nil { return nil, err @@ -678,11 +684,21 @@ func checkEngine(e *Engine) error { return nil } -func createRequest(e *Engine, r Request, bodyReader io.Reader) (*http.Request, error) { - req, err := http.NewRequest(r.Method, r.URL, bodyReader) +func createRequest(e *Engine, r Request, bodyReader io.Reader) (*http.Request, context.CancelFunc, error) { + var err error + var req *http.Request + var cancel context.CancelFunc + + if r.Timeout != 0 { + var ctx context.Context + ctx, cancel = context.WithTimeout(context.TODO(), r.Timeout) + req, err = http.NewRequestWithContext(ctx, r.Method, r.URL, bodyReader) + } else { + req, err = http.NewRequest(r.Method, r.URL, bodyReader) + } if err != nil { - return nil, RequestError{ERROR_CREATE_REQUEST, err.Error()} + return nil, nil, RequestError{ERROR_CREATE_REQUEST, err.Error()} } if r.Headers != nil && len(r.Headers) != 0 { @@ -715,7 +731,7 @@ func createRequest(e *Engine, r Request, bodyReader io.Reader) (*http.Request, e req.Close = true } - return req, nil + return req, cancel, nil } func configureMultipartRequest(r *Request, file, fieldName string, extraFields map[string]string) error { diff --git a/req/req_test.go b/req/req_test.go index 1ec28a88..0c5e9b7f 100644 --- a/req/req_test.go +++ b/req/req_test.go @@ -42,6 +42,7 @@ const ( _URL_STRING_RESP = "/string-response" _URL_JSON_RESP = "/json-response" _URL_DISCARD = "/discard" + _URL_TIMEOUT = "/timeout" ) const ( @@ -421,6 +422,15 @@ func (s *ReqSuite) TestDiscard(c *C) { c.Assert(resp.StatusCode, Equals, 500) } +func (s *ReqSuite) TestTimeout(c *C) { + _, err := Request{ + URL: s.url + _URL_TIMEOUT, + Timeout: 10 * time.Millisecond, + }.Do() + + c.Assert(err, NotNil) +} + func (s *ReqSuite) TestEncoding(c *C) { resp, err := Request{ URL: s.url + "/404", @@ -582,13 +592,76 @@ func (s *ReqSuite) TestQueryEncoding(c *C) { } func (s *ReqSuite) TestLimiter(c *C) { - var l *limiter + var l *Limiter - c.Assert(createLimiter(0.0), IsNil) + c.Assert(NewLimiter(0.0), IsNil) l.Wait() } +func (s *ReqSuite) TestRetrier(c *C) { + r := NewRetrier(Global) + + resp, err := r.Get( + Request{URL: s.url + _URL_GET}, + Retry{Num: 3}, + ) + + c.Assert(err, IsNil) + c.Assert(resp, NotNil) + + resp, err = r.Get( + Request{URL: "http://127.0.0.1:1"}, + Retry{Num: 3}, + ) + + c.Assert(err, NotNil) + + resp, err = r.Get( + Request{URL: s.url + "/unknown"}, + Retry{Num: 3, Status: STATUS_OK, Pause: time.Millisecond}, + ) + + c.Assert(err, NotNil) + + resp, err = r.Get( + Request{URL: s.url + "/unknown"}, + Retry{Num: 3, MinStatus: 299}, + ) + + c.Assert(err, NotNil) +} + +func (s *ReqSuite) TestRetrierErrors(c *C) { + var r *Retrier + + _, err := r.Do(Request{}, Retry{Num: 10}) + c.Assert(err, Equals, ErrNilRetrier) + _, err = r.Get(Request{}, Retry{Num: 10}) + c.Assert(err, Equals, ErrNilRetrier) + _, err = r.Post(Request{}, Retry{Num: 10}) + c.Assert(err, Equals, ErrNilRetrier) + _, err = r.Put(Request{}, Retry{Num: 10}) + c.Assert(err, Equals, ErrNilRetrier) + _, err = r.Head(Request{}, Retry{Num: 10}) + c.Assert(err, Equals, ErrNilRetrier) + _, err = r.Patch(Request{}, Retry{Num: 10}) + c.Assert(err, Equals, ErrNilRetrier) + _, err = r.Delete(Request{}, Retry{Num: 10}) + c.Assert(err, Equals, ErrNilRetrier) + + r = &Retrier{} + _, err = r.Do(Request{}, Retry{Num: 10}) + c.Assert(err, Equals, ErrNilEngine) + + c.Assert(Retry{Num: 3}.Validate(), IsNil) + c.Assert(Retry{Num: -1}.Validate(), NotNil) + c.Assert(Retry{Num: 3, Status: 20}.Validate(), NotNil) + c.Assert(Retry{Num: 3, Status: 2000}.Validate(), NotNil) + c.Assert(Retry{Num: 3, MinStatus: 20}.Validate(), NotNil) + c.Assert(Retry{Num: 3, MinStatus: 2000}.Validate(), NotNil) +} + func (s *ReqSuite) TestNil(c *C) { var e *Engine @@ -669,6 +742,7 @@ func runHTTPServer(s *ReqSuite, c *C) { server.Handler.(*http.ServeMux).HandleFunc(_URL_STRING_RESP, stringRespRequestHandler) server.Handler.(*http.ServeMux).HandleFunc(_URL_JSON_RESP, jsonRespRequestHandler) server.Handler.(*http.ServeMux).HandleFunc(_URL_DISCARD, discardRequestHandler) + server.Handler.(*http.ServeMux).HandleFunc(_URL_TIMEOUT, timeoutRequestHandler) err = server.Serve(listener) @@ -949,3 +1023,9 @@ func discardRequestHandler(w http.ResponseWriter, r *http.Request) { "boolean": true }`, )) } + +func timeoutRequestHandler(w http.ResponseWriter, r *http.Request) { + time.Sleep(100 * time.Millisecond) + w.WriteHeader(200) + w.Write([]byte(`{}`)) +} diff --git a/req/retrier.go b/req/retrier.go new file mode 100644 index 00000000..8dc47e32 --- /dev/null +++ b/req/retrier.go @@ -0,0 +1,145 @@ +package req + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2024 ESSENTIAL KAOS // +// Apache License, Version 2.0 // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "fmt" + "time" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// Retrier is retrier struct +type Retrier struct { + e *Engine +} + +// Retry contains retry configuration +type Retry struct { + Num int // Number of tries (1 or more) + Pause time.Duration // Pause between tries + Status int // Required HTTP status (100-599) + MinStatus int // Minimal HTTP status number (100-599) +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +var ( + // ErrNilEngine is returned if retrier struct is nil + ErrNilRetrier = fmt.Errorf("Retrier is nil") +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// NewRetrier creates new retrier instance +func NewRetrier(e ...*Engine) *Retrier { + engine := Global + + if len(e) != 0 { + engine = e[0] + } + + return &Retrier{e: engine} +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// Delete tries to send given request +func (rt *Retrier) Do(r Request, rr Retry) (*Response, error) { + return rt.doRequest("", r, rr) +} + +// Delete tries to send GET request +func (rt *Retrier) Get(r Request, rr Retry) (*Response, error) { + return rt.doRequest(GET, r, rr) +} + +// Delete tries to send POST request +func (rt *Retrier) Post(r Request, rr Retry) (*Response, error) { + return rt.doRequest(POST, r, rr) +} + +// Delete tries to send PUT request +func (rt *Retrier) Put(r Request, rr Retry) (*Response, error) { + return rt.doRequest(PUT, r, rr) +} + +// Delete tries to send HEAD request +func (rt *Retrier) Head(r Request, rr Retry) (*Response, error) { + return rt.doRequest(HEAD, r, rr) +} + +// Delete tries to send PATCH request +func (rt *Retrier) Patch(r Request, rr Retry) (*Response, error) { + return rt.doRequest(PATCH, r, rr) +} + +// Delete tries to send DELETE request +func (rt *Retrier) Delete(r Request, rr Retry) (*Response, error) { + return rt.doRequest(DELETE, r, rr) +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// Validate validates retry configuration +func (r Retry) Validate() error { + switch { + case r.Num < 1: + return fmt.Errorf("Number of tries must be equal or greater that 1 (%d < 1)", r.Num) + case r.Status != 0 && (r.Status < 100 || r.Status > 599): + return fmt.Errorf("Invalid HTTP status code %d", r.Status) + case r.MinStatus != 0 && (r.MinStatus < 100 || r.MinStatus > 599): + return fmt.Errorf("Invalid minimal HTTP status code %d", r.MinStatus) + } + + return nil +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +func (rt *Retrier) doRequest(method string, r Request, rr Retry) (*Response, error) { + switch { + case rt == nil: + return nil, ErrNilRetrier + case rt.e == nil: + return nil, ErrNilEngine + } + + var lastErr error + + for i := 0; i < rr.Num; i++ { + resp, err := rt.e.doRequest(r, method) + + if err != nil { + lastErr = err + } else { + switch { + case rr.Status != 0 && resp.StatusCode != rr.Status: + lastErr = fmt.Errorf( + "All requests completed with non-ok status code (%d is required)", + rr.Status, + ) + case rr.MinStatus != 0 && resp.StatusCode > rr.MinStatus: + lastErr = fmt.Errorf( + "All requests completed with non-ok status code (status code must be greater than %d)", + rr.Status, + ) + } + } + + if lastErr == nil { + return resp, nil + } + + if rr.Pause > 0 { + time.Sleep(rr.Pause) + } + } + + return nil, lastErr +} diff --git a/strutil/example_test.go b/strutil/example_test.go index f77177aa..a4ce5e91 100644 --- a/strutil/example_test.go +++ b/strutil/example_test.go @@ -145,10 +145,10 @@ func ExampleReplaceIgnoreCase() { } func ExampleFields() { - fmt.Printf("%#v\n", Fields("Bob Alice, 'Mary Key', \"John Dow\"")) + fmt.Printf("%#v\n", Fields(`Bob Alice, 'Mary Key', "John \"Big John\" Dow"`)) // Output: - // []string{"Bob", "Alice", "Mary Key", "John Dow"} + // []string{"Bob", "Alice", "Mary Key", "John \"Big John\" Dow"} } func ExampleReadField() { diff --git a/strutil/fuzz.go b/strutil/fuzz.go new file mode 100644 index 00000000..8869ea01 --- /dev/null +++ b/strutil/fuzz.go @@ -0,0 +1,16 @@ +//go:build gofuzz +// +build gofuzz + +package strutil + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2024 ESSENTIAL KAOS // +// Apache License, Version 2.0 // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +func Fuzz(data []byte) int { + Fields(string(data)) + return 0 +} diff --git a/strutil/strutil.go b/strutil/strutil.go index e829ba70..c773d0f9 100644 --- a/strutil/strutil.go +++ b/strutil/strutil.go @@ -365,37 +365,47 @@ MAINLOOP: // consecutive white space or comma characters func Fields(data string) []string { var result []string - var item strings.Builder + var buf bytes.Buffer var waitChar rune + var escaped bool for _, char := range data { switch char { + case '\\': + buf.WriteRune(char) + escaped = true case '"', '\'', '`', '“', '”', '‘', '’', '«', '»', '„': - if waitChar == 0 { + if waitChar == 0 && !escaped { waitChar = getClosingChar(char) - } else if waitChar != 0 && waitChar == char { - result = appendField(result, item.String()) - item.Reset() + } else if waitChar != 0 && waitChar == char && !escaped { + result = appendField(result, buf.String()) + buf.Reset() waitChar = 0 } else { - item.WriteRune(char) + if escaped && buf.Len() > 0 { + buf.Truncate(buf.Len() - 1) + } + buf.WriteRune(char) + escaped = false } case ',', ';', ' ': if waitChar != 0 { - item.WriteRune(char) + buf.WriteRune(char) + escaped = false } else { - result = appendField(result, item.String()) - item.Reset() + result = appendField(result, buf.String()) + buf.Reset() } default: - item.WriteRune(char) + buf.WriteRune(char) + escaped = false } } - if item.Len() != 0 { - result = appendField(result, item.String()) + if buf.Len() != 0 { + result = appendField(result, buf.String()) } return result diff --git a/strutil/strutil_test.go b/strutil/strutil_test.go index 59657fc6..b5ffc6c7 100644 --- a/strutil/strutil_test.go +++ b/strutil/strutil_test.go @@ -140,13 +140,13 @@ func (s *StrUtilSuite) TestFields(c *C) { c.Assert(Fields("1,2,3,4,5"), DeepEquals, []string{"1", "2", "3", "4", "5"}) c.Assert(Fields("1;2;3;4;5"), DeepEquals, []string{"1", "2", "3", "4", "5"}) c.Assert(Fields("1, 2, 3, 4, 5"), DeepEquals, []string{"1", "2", "3", "4", "5"}) - c.Assert(Fields("\"1 2\" 3 \"4 5\""), DeepEquals, []string{"1 2", "3", "4 5"}) + c.Assert(Fields(`"1 2" 3 "4 5"`), DeepEquals, []string{"1 2", "3", "4 5"}) c.Assert(Fields("'1 2' 3 '4 5'"), DeepEquals, []string{"1 2", "3", "4 5"}) c.Assert(Fields("‘1 2’ 3 ‘4 5’"), DeepEquals, []string{"1 2", "3", "4 5"}) c.Assert(Fields("“1 2” 3 “4 5”"), DeepEquals, []string{"1 2", "3", "4 5"}) c.Assert(Fields("„1 2“ 3 «4 5»"), DeepEquals, []string{"1 2", "3", "4 5"}) c.Assert(Fields("«1 '2'» 3 «4 “5”»"), DeepEquals, []string{"1 '2'", "3", "4 “5”"}) - c.Assert(Fields("Bob Alice, 'Mary Key', \"John 'Dow'\""), DeepEquals, []string{"Bob", "Alice", "Mary Key", "John 'Dow'"}) + c.Assert(Fields(`Bob Alice, 'Mary Key', "John \"Big John\" 'Dow'"`), DeepEquals, []string{"Bob", "Alice", "Mary Key", "John \"Big John\" 'Dow'"}) } func (s *StrUtilSuite) TestReadField(c *C) { @@ -260,7 +260,7 @@ func (s *StrUtilSuite) BenchmarkSize(c *C) { func (s *StrUtilSuite) BenchmarkFields(c *C) { for i := 0; i < c.N; i++ { - Fields("\"1 2\" 3 \"4 5\"") + Fields(`"123" 59 "31" '2'`) } } diff --git a/version.go b/version.go index 21a80d34..a4588fab 100644 --- a/version.go +++ b/version.go @@ -8,4 +8,4 @@ package ek // ////////////////////////////////////////////////////////////////////////////////// // // VERSION is current ek package version -const VERSION = "13.11.0" +const VERSION = "13.12.0"