├── .gitignore ├── Makefile ├── Godeps ├── Readme └── Godeps.json ├── .travis.yml ├── error_test.go ├── error.go ├── job.go ├── README.md ├── client.go ├── LICENSE └── client_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | deps: 2 | go get github.com/tools/godep && godep restore 3 | 4 | test: 5 | go test -v -race ./... 6 | -------------------------------------------------------------------------------- /Godeps/Readme: -------------------------------------------------------------------------------- 1 | This directory tree is generated automatically by godep. 2 | 3 | Please do not edit. 4 | 5 | See https://github.com/tools/godep for more information. 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.7 5 | 6 | before_install: 7 | - go get github.com/mattn/goveralls 8 | - go get golang.org/x/tools/cmd/cover 9 | 10 | install: make deps 11 | 12 | script: 13 | - $HOME/gopath/bin/goveralls -service=travis-ci 14 | -------------------------------------------------------------------------------- /Godeps/Godeps.json: -------------------------------------------------------------------------------- 1 | { 2 | "ImportPath": "github.com/iamduo/go-workq", 3 | "GoVersion": "go1.7", 4 | "GodepVersion": "v60", 5 | "Deps": [ 6 | { 7 | "ImportPath": "github.com/satori/go.uuid", 8 | "Comment": "v1.0.0", 9 | "Rev": "f9ab0dce87d815821e221626b772e3475a0d2749" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package workq 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestResponseError(t *testing.T) { 8 | err := NewResponseError("CODE", "TEXT") 9 | rerr := err.(*ResponseError) 10 | if err.Error() != "CODE TEXT" || rerr.Code() != "CODE" || rerr.Text() != "TEXT" { 11 | t.Fatalf("Error mismatch, err=%+v", rerr) 12 | } 13 | } 14 | 15 | func TestNetError(t *testing.T) { 16 | err := NewNetError("bad") 17 | _, ok := err.(*NetError) 18 | if err.Error() != "Net Error: bad" || !ok { 19 | t.Fatalf("Error mismatch, err=%s", err) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package workq 2 | 3 | type ResponseError struct { 4 | code string 5 | text string 6 | } 7 | 8 | func NewResponseError(code string, text string) error { 9 | return &ResponseError{code: code, text: text} 10 | } 11 | 12 | func (e *ResponseError) Error() string { 13 | if e.text != "" { 14 | return e.code + " " + e.text 15 | } 16 | 17 | return e.code 18 | } 19 | 20 | func (e *ResponseError) Code() string { 21 | return e.code 22 | } 23 | 24 | func (e *ResponseError) Text() string { 25 | return e.text 26 | } 27 | 28 | type NetError struct { 29 | text string 30 | } 31 | 32 | func (e *NetError) Error() string { 33 | return "Net Error: " + e.text 34 | } 35 | 36 | func NewNetError(text string) error { 37 | return &NetError{text: text} 38 | } 39 | -------------------------------------------------------------------------------- /job.go: -------------------------------------------------------------------------------- 1 | package workq 2 | 3 | // FgJob is executed by the "run" command. 4 | // Describes a foreground job specification. 5 | type FgJob struct { 6 | ID string 7 | Name string 8 | TTR int 9 | Timeout int // Milliseconds to wait for job completion. 10 | Payload []byte 11 | Priority int // Numeric priority 12 | } 13 | 14 | // BgJob is executed by the "add" command. 15 | // Describes a background job specification. 16 | type BgJob struct { 17 | ID string 18 | Name string 19 | TTR int // Time-to-run 20 | TTL int // Time-to-live 21 | Payload []byte 22 | Priority int // Numeric priority 23 | MaxAttempts int // Absoulute max num of attempts. 24 | MaxFails int // Absolute max number of failures. 25 | } 26 | 27 | // ScheduledJob is executed by the "schedule" command. 28 | // Describes a scheduled job specification. 29 | type ScheduledJob struct { 30 | ID string 31 | Name string 32 | TTR int 33 | TTL int 34 | Payload []byte 35 | Time string 36 | Priority int // Numeric priority 37 | MaxAttempts int // Absoulute max num of attempts. 38 | MaxFails int // Absolute max number of failures. 39 | } 40 | 41 | // LeasedJob is returned by the "lease" command. 42 | type LeasedJob struct { 43 | ID string 44 | Name string 45 | TTR int 46 | Payload []byte 47 | } 48 | 49 | // JobResult is returned by the "run" & "result" commands. 50 | type JobResult struct { 51 | Success bool 52 | Result []byte 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-workq [![Build Status](https://travis-ci.org/iamduo/go-workq.svg?branch=master)](https://travis-ci.org/iamduo/go-workq) [![Coverage Status](https://coveralls.io/repos/github/iamduo/go-workq/badge.svg?branch=master)](https://coveralls.io/github/iamduo/go-workq?branch=master) ![GitHub Logo](https://img.shields.io/badge/status-alpha-yellow.svg) 2 | 3 | 4 | Go client for [Workq](https://github.com/iamduo/workq). 5 | 6 | **Table of Contents** 7 | 8 | - [Connecting](#connecting) 9 | - [Closing active connection](#closing-active-connection) 10 | - [Client Commands](#client-commands) 11 | - [Add](#add) 12 | - [Run](#run) 13 | - [Schedule](#schedule) 14 | - [Result](#result) 15 | - [Worker Commands](#worker-commands) 16 | - [Lease](#lease) 17 | - [Complete](#complete) 18 | - [Fail](#fail) 19 | - [Adminstrative Commands](#adminstrative-commands) 20 | - [Delete](#delete) 21 | - [Inspect](#inspect) 22 | 23 | ## Connection Management 24 | 25 | ### Connecting 26 | 27 | ```go 28 | client, err := workq.Connect("localhost:9922") 29 | if err != nil { 30 | // ... 31 | } 32 | ``` 33 | 34 | ### Closing active connection 35 | 36 | ```go 37 | err := client.Close() 38 | if err != nil { 39 | // ... 40 | } 41 | ``` 42 | 43 | ## Commands [![Protocol Doc](https://img.shields.io/badge/protocol-doc-516EA9.svg)](https://github.com/iamduo/workq/blob/master/doc/protocol.md#commands) [![GoDoc](https://godoc.org/github.com/iamduo/go-workq?status.svg)](https://godoc.org/github.com/iamduo/go-workq) 44 | 45 | ### Client Commands 46 | 47 | #### Add 48 | 49 | [Protocol Doc](https://github.com/iamduo/workq/blob/master/doc/protocol.md#add) | [Go Doc](https://godoc.org/github.com/iamduo/go-workq#Client.Add) 50 | 51 | Add a background job. The result can be retrieved through the ["result"](#result) command. 52 | 53 | ```go 54 | job := &workq.BgJob{ 55 | ID: "61a444a0-6128-41c0-8078-cc757d3bd2d8", 56 | Name: "ping", 57 | TTR: 5000, // 5 second time-to-run limit 58 | TTL: 60000, // Expire after 60 seconds 59 | Payload: []byte("Ping!"), 60 | Priority: 10, // @OPTIONAL Numeric priority, default 0. 61 | MaxAttempts: 3, // @OPTIONAL Absolute max num of attempts. 62 | MaxFails: 1, // @OPTIONAL Absolute max number of failures. 63 | } 64 | err := client.Add(job) 65 | if err != nil { 66 | // ... 67 | } 68 | ``` 69 | 70 | #### Run 71 | 72 | [Protocol Doc](https://github.com/iamduo/workq/blob/master/doc/protocol.md#run) | [Go Doc](https://godoc.org/github.com/iamduo/go-workq#Client.Run) 73 | 74 | Run a job and wait for its result. 75 | 76 | ```go 77 | job := &workq.FgJob{ 78 | ID: "61a444a0-6128-41c0-8078-cc757d3bd2d8", 79 | Name: "ping", 80 | TTR: 5000, // 5 second time-to-run limit 81 | Timeout: 60000, // Wait up to 60 seconds for a worker to pick up. 82 | Payload: []byte("Ping!"), 83 | Priority: 10, // @OPTIONAL Numeric priority, default 0. 84 | } 85 | result, err := client.Run(job) 86 | if err != nil { 87 | // ... 88 | } 89 | 90 | fmt.Printf("Success: %t, Result: %s", result.Success, result.Result) 91 | ``` 92 | 93 | #### Schedule 94 | 95 | [Protocol Doc](https://github.com/iamduo/workq/blob/master/doc/protocol.md#schedule) | [Go Doc](https://godoc.org/github.com/iamduo/go-workq#Client.Schedule) 96 | 97 | Schedule a job at a UTC time. The result can be retrieved through the ["result"](#result) command. 98 | 99 | ```go 100 | job := &workq.ScheduledJob{ 101 | ID: "61a444a0-6128-41c0-8078-cc757d3bd2d8", 102 | Name: "ping", 103 | Time: "2016-12-01T00:00:00Z", // Start job at this UTC time. 104 | TTL: 60000, // Expire after 60 seconds 105 | TTR: 5000, // 5 second time-to-run limit 106 | Payload: []byte("Ping!"), 107 | Priority: 10, // @OPTIONAL Numeric priority, default 0. 108 | MaxAttempts: 3, // @OPTIONAL Absolute max num of attempts. 109 | MaxFails: 1, // @OPTIONAL Absolute max number of failures. 110 | } 111 | err := client.Schedule(job) 112 | if err != nil { 113 | // ... 114 | } 115 | ``` 116 | 117 | #### Result 118 | 119 | [Protocol Doc](https://github.com/iamduo/workq/blob/master/doc/protocol.md#result) | [Go Doc](https://godoc.org/github.com/iamduo/go-workq#Client.Result) 120 | 121 | Get a job result previously executed by [Add](#add) or [Schedule](#schedule) commands. 122 | 123 | ```go 124 | // Get a job result, waiting up to 60 seconds if the job is still executing. 125 | result, err := client.Result("61a444a0-6128-41c0-8078-cc757d3bd2d8", 60000) 126 | if err != nil { 127 | // ... 128 | } 129 | 130 | fmt.Printf("Success: %t, Result: %s", result.Success, result.Result) 131 | ``` 132 | 133 | ### Worker Commands 134 | 135 | #### Lease 136 | 137 | [Protocol Doc](https://github.com/iamduo/workq/blob/master/doc/protocol.md#lease) | [Go Doc](https://godoc.org/github.com/iamduo/go-workq#Client.Lease) 138 | 139 | Lease a job within a set of one or more names with a wait-timeout (milliseconds). 140 | 141 | ```go 142 | // Lease the first available job in "ping1", "ping2", "ping3" 143 | // waiting up to 60 seconds. 144 | job, err := client.Lease([]string{"ping1", "ping2", "ping3"}, 60000) 145 | if err != nil { 146 | // ... 147 | } 148 | 149 | fmt.Printf("Leased Job: ID: %s, Name: %s, Payload: %s", job.ID, job.Name, job.Payload) 150 | ``` 151 | 152 | #### Complete 153 | 154 | [Protocol Doc](https://github.com/iamduo/workq/blob/master/doc/protocol.md#complete) | [Go Doc](https://godoc.org/github.com/iamduo/go-workq#Client.Complete) 155 | 156 | Mark a job successfully completed with a result. 157 | 158 | ```go 159 | err := client.Complete("61a444a0-6128-41c0-8078-cc757d3bd2d8", []byte("Pong!")) 160 | if err != nil { 161 | // ... 162 | } 163 | ``` 164 | 165 | #### Fail 166 | 167 | [Protocol Doc](https://github.com/iamduo/workq/blob/master/doc/protocol.md#fail) | [Go Doc](https://godoc.org/github.com/iamduo/go-workq#Client.Fail) 168 | 169 | Mark a job failed with a result. 170 | 171 | ```go 172 | err := client.Fail("61a444a0-6128-41c0-8078-cc757d3bd2d8", []byte("Failed-Pong!")) 173 | if err != nil { 174 | // ... 175 | } 176 | ``` 177 | 178 | ### Adminstrative Commands 179 | 180 | #### Delete 181 | 182 | [Protocol Doc](https://github.com/iamduo/workq/blob/master/doc/protocol.md#delete) | [Go Doc](https://godoc.org/github.com/iamduo/go-workq#Client.Delete) 183 | 184 | 185 | ```go 186 | err := client.Delete("61a444a0-6128-41c0-8078-cc757d3bd2d8") 187 | if err != nil { 188 | // ... 189 | } 190 | ``` 191 | 192 | 193 | #### Inspect 194 | 195 | [Protocol Doc](https://github.com/iamduo/workq/blob/master/doc/protocol.md#inspect) 196 | 197 | Inspect commands not yet supported yet. 198 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | // Package workq implements Workq protocol commands: 2 | // https://github.com/iamduo/workq/blob/master/doc/protocol.md#commands 3 | package workq 4 | 5 | import ( 6 | "bufio" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net" 11 | "regexp" 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/satori/go.uuid" 16 | ) 17 | 18 | var ( 19 | // ErrMalformed is returned when responses from workq can not be parsed 20 | // due to unrecognized responses. 21 | ErrMalformed = errors.New("Malformed response") 22 | ) 23 | 24 | const ( 25 | // Max Data Block that can be read within a response, 1 MiB. 26 | maxDataBlock = 1048576 27 | 28 | // Line terminator in string form. 29 | crnl = "\r\n" 30 | termLen = 2 31 | 32 | // Time format for any date times. Compatible with time.Format. 33 | TimeFormat = "2006-01-02T15:04:05Z" 34 | ) 35 | 36 | // Client represents a single connection to Workq. 37 | type Client struct { 38 | conn net.Conn 39 | rdr *bufio.Reader 40 | parser *responseParser 41 | } 42 | 43 | // Connect to a Workq server returning a Client 44 | func Connect(addr string) (*Client, error) { 45 | conn, err := net.Dial("tcp", addr) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return NewClient(conn), nil 51 | } 52 | 53 | // NewClient returns a Client from a net.Conn. 54 | func NewClient(conn net.Conn) *Client { 55 | rdr := bufio.NewReader(conn) 56 | return &Client{ 57 | conn: conn, 58 | rdr: rdr, 59 | parser: &responseParser{rdr: rdr}, 60 | } 61 | } 62 | 63 | // "add" command: https://github.com/iamduo/workq/blob/master/doc/protocol.md#add 64 | // 65 | // Add background job 66 | // Returns ResponseError for Workq response errors. 67 | // Returns NetError on any network errors. 68 | // Returns ErrMalformed if response can't be parsed. 69 | func (c *Client) Add(j *BgJob) error { 70 | var flagsPad string 71 | var flags []string 72 | if j.Priority != 0 { 73 | flags = append(flags, fmt.Sprintf("-priority=%d", j.Priority)) 74 | } 75 | if j.MaxAttempts != 0 { 76 | flags = append(flags, fmt.Sprintf("-max-attempts=%d", j.MaxAttempts)) 77 | } 78 | if j.MaxFails != 0 { 79 | flags = append(flags, fmt.Sprintf("-max-fails=%d", j.MaxFails)) 80 | } 81 | if len(flags) > 0 { 82 | flagsPad = " " 83 | } 84 | r := []byte(fmt.Sprintf( 85 | "add %s %s %d %d %d%s"+crnl+"%s"+crnl, 86 | j.ID, 87 | j.Name, 88 | j.TTR, 89 | j.TTL, 90 | len(j.Payload), 91 | flagsPad+strings.Join(flags, " "), 92 | j.Payload, 93 | )) 94 | _, err := c.conn.Write(r) 95 | if err != nil { 96 | return NewNetError(err.Error()) 97 | } 98 | 99 | return c.parser.parseOk() 100 | } 101 | 102 | // "run" command: https://github.com/iamduo/workq/blob/master/doc/protocol.md#run 103 | // 104 | // Submit foreground job and wait for result. 105 | // Returns ResponseError for Workq response errors 106 | // Returns NetError on any network errors. 107 | // Returns ErrMalformed if response can't be parsed. 108 | func (c *Client) Run(j *FgJob) (*JobResult, error) { 109 | var flags string 110 | if j.Priority != 0 { 111 | flags = fmt.Sprintf(" -priority=%d", j.Priority) 112 | } 113 | r := []byte(fmt.Sprintf( 114 | "run %s %s %d %d %d%s"+crnl+"%s"+crnl, 115 | j.ID, 116 | j.Name, 117 | j.TTR, 118 | j.Timeout, 119 | len(j.Payload), 120 | flags, 121 | j.Payload, 122 | )) 123 | 124 | _, err := c.conn.Write(r) 125 | if err != nil { 126 | return nil, NewNetError(err.Error()) 127 | } 128 | 129 | count, err := c.parser.parseOkWithReply() 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | if count != 1 { 135 | return nil, ErrMalformed 136 | } 137 | 138 | return c.parser.readResult() 139 | } 140 | 141 | // "schedule" command: https://github.com/iamduo/workq/blob/master/doc/protocol.md#schedule 142 | // 143 | // Schedule job at future UTC time. 144 | // Returns ResponseError for Workq response errors. 145 | // Returns NetError on any network errors. 146 | // Returns ErrMalformed if response can't be parsed. 147 | func (c *Client) Schedule(j *ScheduledJob) error { 148 | var flagsPad string 149 | var flags []string 150 | if j.Priority != 0 { 151 | flags = append(flags, fmt.Sprintf("-priority=%d", j.Priority)) 152 | } 153 | if j.MaxAttempts != 0 { 154 | flags = append(flags, fmt.Sprintf("-max-attempts=%d", j.MaxAttempts)) 155 | } 156 | if j.MaxFails != 0 { 157 | flags = append(flags, fmt.Sprintf("-max-fails=%d", j.MaxFails)) 158 | } 159 | if len(flags) > 0 { 160 | flagsPad = " " 161 | } 162 | r := []byte(fmt.Sprintf( 163 | "schedule %s %s %d %d %s %d%s"+crnl+"%s"+crnl, 164 | j.ID, 165 | j.Name, 166 | j.TTR, 167 | j.TTL, 168 | j.Time, 169 | len(j.Payload), 170 | flagsPad+strings.Join(flags, " "), 171 | j.Payload, 172 | )) 173 | _, err := c.conn.Write(r) 174 | if err != nil { 175 | return NewNetError(err.Error()) 176 | } 177 | 178 | return c.parser.parseOk() 179 | } 180 | 181 | // "result" command: https://github.com/iamduo/workq/blob/master/doc/protocol.md#result 182 | // 183 | // Fetch job result, @see PROTOCOL_DOC 184 | // Returns ResponseError for Workq response errors. 185 | // Returns NetError on any network errors. 186 | // Returns ErrMalformed if response can't be parsed. 187 | func (c *Client) Result(id string, timeout int) (*JobResult, error) { 188 | r := []byte(fmt.Sprintf( 189 | "result %s %d"+crnl, 190 | id, 191 | timeout, 192 | )) 193 | _, err := c.conn.Write(r) 194 | if err != nil { 195 | return nil, NewNetError(err.Error()) 196 | } 197 | 198 | count, err := c.parser.parseOkWithReply() 199 | if err != nil { 200 | return nil, err 201 | } 202 | if count != 1 { 203 | return nil, ErrMalformed 204 | } 205 | 206 | return c.parser.readResult() 207 | } 208 | 209 | // "lease" command: https://github.com/iamduo/workq/blob/master/doc/protocol.md#lease 210 | // 211 | // Lease a job, waiting for available jobs until timeout, @see PROTOCOL_DOC 212 | // Returns ResponseError for Workq response errors. 213 | // Returns NetError on any network errors. 214 | // Returns ErrMalformed if response can't be parsed. 215 | func (c *Client) Lease(names []string, timeout int) (*LeasedJob, error) { 216 | r := []byte(fmt.Sprintf( 217 | "lease %s %d"+crnl, 218 | strings.Join(names, " "), 219 | timeout, 220 | )) 221 | 222 | _, err := c.conn.Write(r) 223 | if err != nil { 224 | return nil, NewNetError(err.Error()) 225 | } 226 | 227 | count, err := c.parser.parseOkWithReply() 228 | if err != nil { 229 | return nil, err 230 | } 231 | if count != 1 { 232 | return nil, ErrMalformed 233 | } 234 | 235 | return c.parser.readLeasedJob() 236 | } 237 | 238 | // "complete" command: https://github.com/iamduo/workq/blob/master/doc/protocol.md#complete 239 | // 240 | // Mark job successfully complete, @see PROTOCOL_DOC 241 | // Returns ResponseError for Workq response errors. 242 | // Returns NetError on any network errors. 243 | // Returns ErrMalformed if response can't be parsed. 244 | func (c *Client) Complete(id string, result []byte) error { 245 | r := []byte(fmt.Sprintf( 246 | "complete %s %d"+crnl+"%s"+crnl, 247 | id, 248 | len(result), 249 | result, 250 | )) 251 | _, err := c.conn.Write(r) 252 | if err != nil { 253 | return NewNetError(err.Error()) 254 | } 255 | 256 | return c.parser.parseOk() 257 | } 258 | 259 | // "fail" command: https://github.com/iamduo/workq/blob/master/doc/protocol.md#fail 260 | // 261 | // Mark job as failure. 262 | // Returns ResponseError for Workq response errors. 263 | // Returns NetError on any network errors. 264 | // Returns ErrMalformed if response can't be parsed. 265 | func (c *Client) Fail(id string, result []byte) error { 266 | r := []byte(fmt.Sprintf( 267 | "fail %s %d"+crnl+"%s"+crnl, 268 | id, 269 | len(result), 270 | result, 271 | )) 272 | _, err := c.conn.Write(r) 273 | if err != nil { 274 | return NewNetError(err.Error()) 275 | } 276 | 277 | return c.parser.parseOk() 278 | } 279 | 280 | // "delete" command: https://github.com/iamduo/workq/blob/master/doc/protocol.md#delete 281 | // 282 | // Delete job. 283 | // Returns ResponseError for Workq response errors. 284 | // Returns NetError on any network errors. 285 | // Returns ErrMalformed if response can't be parsed. 286 | func (c *Client) Delete(id string) error { 287 | r := []byte(fmt.Sprintf( 288 | "delete %s"+crnl, 289 | id, 290 | )) 291 | _, err := c.conn.Write(r) 292 | if err != nil { 293 | return NewNetError(err.Error()) 294 | } 295 | 296 | return c.parser.parseOk() 297 | } 298 | 299 | type responseParser struct { 300 | rdr *bufio.Reader 301 | } 302 | 303 | // Close client connection. 304 | func (c *Client) Close() error { 305 | return c.conn.Close() 306 | } 307 | 308 | // Parse "OK\r\n" response. 309 | func (p *responseParser) parseOk() error { 310 | line, err := p.readLine() 311 | if err != nil { 312 | return err 313 | } 314 | 315 | if len(line) < 3 { 316 | return ErrMalformed 317 | } 318 | 319 | sign := string(line[0]) 320 | if sign == "+" && string(line[1:3]) == "OK" && len(line) == 3 { 321 | return nil 322 | } 323 | 324 | if sign != "-" { 325 | return ErrMalformed 326 | } 327 | 328 | err, _ = p.errorFromLine(line) 329 | return err 330 | } 331 | 332 | // Parse "OK \r\n" response. 333 | func (p *responseParser) parseOkWithReply() (int, error) { 334 | line, err := p.readLine() 335 | if err != nil { 336 | return 0, err 337 | } 338 | 339 | if len(line) < 5 { 340 | return 0, ErrMalformed 341 | } 342 | 343 | sign := string(line[0]) 344 | if sign == "+" && string(line[1:3]) == "OK" { 345 | count, err := strconv.Atoi(string(line[4:])) 346 | if err != nil { 347 | return 0, ErrMalformed 348 | } 349 | 350 | return count, nil 351 | } 352 | 353 | if sign != "-" { 354 | return 0, ErrMalformed 355 | } 356 | 357 | err, _ = p.errorFromLine(line) 358 | return 0, err 359 | } 360 | 361 | // Read valid line terminated by "\r\n" 362 | func (p *responseParser) readLine() ([]byte, error) { 363 | line, err := p.rdr.ReadBytes(byte('\n')) 364 | if err != nil { 365 | return nil, NewNetError(err.Error()) 366 | } 367 | 368 | if len(line) < termLen { 369 | return nil, ErrMalformed 370 | } 371 | 372 | if len(line) >= termLen { 373 | if line[len(line)-termLen] != '\r' { 374 | return nil, ErrMalformed 375 | } 376 | 377 | line = line[:len(line)-termLen] 378 | } 379 | 380 | return line, nil 381 | } 382 | 383 | // Read data block up to size terminated by "\r\n" 384 | func (p *responseParser) readBlock(size int) ([]byte, error) { 385 | if size < 0 || size > maxDataBlock { 386 | return nil, ErrMalformed 387 | } 388 | 389 | block := make([]byte, size) 390 | n, err := io.ReadAtLeast(p.rdr, block, size) 391 | if n != size || err != nil { 392 | return nil, ErrMalformed 393 | } 394 | 395 | b := make([]byte, termLen) 396 | n, err = p.rdr.Read(b) 397 | if err != nil || n != termLen || string(b) != crnl { 398 | // Size does not match end of line. 399 | // Trailing garbage is not allowed. 400 | return nil, ErrMalformed 401 | } 402 | 403 | return block, nil 404 | } 405 | 406 | // Read job result consisting of 2 separate terminated lines. 407 | // " \r\n 408 | // \r\n" 409 | func (p *responseParser) readResult() (*JobResult, error) { 410 | line, err := p.readLine() 411 | split := strings.Split(string(line), " ") 412 | if len(split) != 3 { 413 | return nil, ErrMalformed 414 | } 415 | 416 | if split[1] != "0" && split[1] != "1" { 417 | return nil, ErrMalformed 418 | } 419 | 420 | result := &JobResult{} 421 | if split[1] == "1" { 422 | result.Success = true 423 | } 424 | 425 | resultLen, err := strconv.ParseUint(split[2], 10, 64) 426 | if err != nil { 427 | return nil, ErrMalformed 428 | } 429 | 430 | result.Result, err = p.readBlock(int(resultLen)) 431 | if err != nil { 432 | return nil, err 433 | } 434 | 435 | return result, nil 436 | } 437 | 438 | // Read leased job consisting of 2 separate terminated lines. 439 | // " \r\n 440 | // 0 && l <= 128 && nameRe.MatchString(name) { 517 | return name, nil 518 | } 519 | 520 | return "", ErrMalformed 521 | } 522 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package workq 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "net" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestConnectAndClose(t *testing.T) { 12 | addr := "localhost:9944" 13 | _, err := Connect(addr) 14 | if err == nil { 15 | t.Fatalf("Unexpected connect") 16 | } 17 | 18 | server, err := net.Listen("tcp", addr) 19 | if err != nil { 20 | t.Fatalf("Unable to start test server, err=%s", err) 21 | } 22 | defer server.Close() 23 | 24 | client, err := Connect(addr) 25 | if err != nil { 26 | t.Fatalf("Unable to connect, err=%s", err) 27 | } 28 | 29 | err = client.Close() 30 | if err != nil { 31 | t.Fatalf("Unable to close, err=%s", err) 32 | } 33 | 34 | err = client.Close() 35 | if err == nil { 36 | t.Fatal("Expected error on double close") 37 | } 38 | } 39 | 40 | func TestAdd(t *testing.T) { 41 | conn := &TestConn{ 42 | rdr: bytes.NewBuffer([]byte("+OK\r\n")), 43 | wrt: bytes.NewBuffer([]byte("")), 44 | } 45 | client := NewClient(conn) 46 | j := &BgJob{ 47 | ID: "6ba7b810-9dad-11d1-80b4-00c04fd430c4", 48 | Name: "j1", 49 | TTR: 60, 50 | TTL: 60000, 51 | Payload: []byte("a"), 52 | } 53 | err := client.Add(j) 54 | if err != nil { 55 | t.Fatalf("Response mismatch, err=%s", err) 56 | } 57 | 58 | expWrite := []byte( 59 | "add 6ba7b810-9dad-11d1-80b4-00c04fd430c4 j1 60 60000 1\r\na\r\n", 60 | ) 61 | if !bytes.Equal(expWrite, conn.wrt.Bytes()) { 62 | t.Fatalf("Write mismatch, act=%q", conn.wrt.Bytes()) 63 | } 64 | } 65 | 66 | func TestAddOptionalFlags(t *testing.T) { 67 | tests := []struct { 68 | job *BgJob 69 | expWrite []byte 70 | }{ 71 | { 72 | job: &BgJob{ 73 | ID: "6ba7b810-9dad-11d1-80b4-00c04fd430c4", 74 | Name: "j1", 75 | TTR: 1, 76 | TTL: 2, 77 | Payload: []byte(""), 78 | Priority: 100, 79 | }, 80 | expWrite: []byte( 81 | "add 6ba7b810-9dad-11d1-80b4-00c04fd430c4 j1 1 2 0 -priority=100\r\n\r\n", 82 | ), 83 | }, 84 | { 85 | job: &BgJob{ 86 | ID: "6ba7b810-9dad-11d1-80b4-00c04fd430c4", 87 | Name: "j1", 88 | TTR: 1, 89 | TTL: 2, 90 | Payload: []byte(""), 91 | MaxAttempts: 3, 92 | }, 93 | expWrite: []byte( 94 | "add 6ba7b810-9dad-11d1-80b4-00c04fd430c4 j1 1 2 0 -max-attempts=3\r\n\r\n", 95 | ), 96 | }, 97 | { 98 | job: &BgJob{ 99 | ID: "6ba7b810-9dad-11d1-80b4-00c04fd430c4", 100 | Name: "j1", 101 | TTR: 1, 102 | TTL: 2, 103 | Payload: []byte(""), 104 | MaxFails: 3, 105 | }, 106 | expWrite: []byte( 107 | "add 6ba7b810-9dad-11d1-80b4-00c04fd430c4 j1 1 2 0 -max-fails=3\r\n\r\n", 108 | ), 109 | }, 110 | { 111 | job: &BgJob{ 112 | ID: "6ba7b810-9dad-11d1-80b4-00c04fd430c4", 113 | Name: "j1", 114 | TTR: 1, 115 | TTL: 2, 116 | Payload: []byte(""), 117 | Priority: 1, 118 | MaxAttempts: 3, 119 | MaxFails: 1, 120 | }, 121 | expWrite: []byte( 122 | "add 6ba7b810-9dad-11d1-80b4-00c04fd430c4 j1 1 2 0 -priority=1 -max-attempts=3 -max-fails=1\r\n\r\n", 123 | ), 124 | }, 125 | } 126 | 127 | for _, tt := range tests { 128 | conn := &TestConn{ 129 | rdr: bytes.NewBuffer([]byte("+OK\r\n")), 130 | wrt: bytes.NewBuffer([]byte("")), 131 | } 132 | client := NewClient(conn) 133 | err := client.Add(tt.job) 134 | if err != nil { 135 | t.Fatalf("Response mismatch, err=%s", err) 136 | } 137 | 138 | if !bytes.Equal(tt.expWrite, conn.wrt.Bytes()) { 139 | t.Fatalf("Write mismatch, act=%q", conn.wrt.Bytes()) 140 | } 141 | } 142 | } 143 | 144 | func TestAddErrors(t *testing.T) { 145 | tests := []RespErrTestCase{ 146 | { 147 | resp: []byte("-CLIENT-ERROR Invalid Job ID\r\n"), 148 | expErr: errors.New("CLIENT-ERROR Invalid Job ID"), 149 | }, 150 | } 151 | 152 | tests = append(tests, invalidCommonErrorTests()...) 153 | 154 | for _, tt := range tests { 155 | conn := &TestConn{ 156 | rdr: bytes.NewBuffer(tt.resp), 157 | wrt: bytes.NewBuffer([]byte("")), 158 | } 159 | client := NewClient(conn) 160 | j := &BgJob{} 161 | err := client.Add(j) 162 | if err == nil || tt.expErr == nil || err.Error() != tt.expErr.Error() { 163 | t.Fatalf("Response mismatch, err=%q", err) 164 | } 165 | } 166 | } 167 | 168 | func TestAddBadConnError(t *testing.T) { 169 | conn := &TestBadWriteConn{} 170 | client := NewClient(conn) 171 | j := &BgJob{} 172 | err := client.Add(j) 173 | if _, ok := err.(*NetError); !ok { 174 | t.Fatalf("Error mismatch, err=%+v", err) 175 | } 176 | } 177 | 178 | func TestRun(t *testing.T) { 179 | conn := &TestConn{ 180 | rdr: bytes.NewBuffer([]byte( 181 | "+OK 1\r\n" + 182 | "6ba7b810-9dad-11d1-80b4-00c04fd430c4 1 1\r\n" + 183 | "a\r\n", 184 | )), 185 | wrt: bytes.NewBuffer([]byte("")), 186 | } 187 | client := NewClient(conn) 188 | j := &FgJob{ 189 | ID: "6ba7b810-9dad-11d1-80b4-00c04fd430c4", 190 | Name: "j1", 191 | TTR: 5000, 192 | Timeout: 1000, 193 | Payload: []byte("a"), 194 | } 195 | result, err := client.Run(j) 196 | if err != nil { 197 | t.Fatalf("Response mismatch, err=%s", err) 198 | } 199 | 200 | if !result.Success { 201 | t.Fatalf("Success mismatch") 202 | } 203 | 204 | if !bytes.Equal([]byte("a"), result.Result) { 205 | t.Fatalf("Result mismatch") 206 | } 207 | 208 | expWrite := []byte( 209 | "run 6ba7b810-9dad-11d1-80b4-00c04fd430c4 j1 5000 1000 1\r\na\r\n", 210 | ) 211 | if !bytes.Equal(expWrite, conn.wrt.Bytes()) { 212 | t.Fatalf("Write mismatch, act=%s", conn.wrt.Bytes()) 213 | } 214 | } 215 | 216 | func TestRunOptionalFlags(t *testing.T) { 217 | conn := &TestConn{ 218 | rdr: bytes.NewBuffer([]byte( 219 | "+OK 1\r\n" + 220 | "6ba7b810-9dad-11d1-80b4-00c04fd430c4 1 1\r\n" + 221 | "a\r\n", 222 | )), 223 | wrt: bytes.NewBuffer([]byte("")), 224 | } 225 | client := NewClient(conn) 226 | j := &FgJob{ 227 | ID: "6ba7b810-9dad-11d1-80b4-00c04fd430c4", 228 | Name: "j1", 229 | TTR: 5000, 230 | Timeout: 1000, 231 | Payload: []byte("a"), 232 | Priority: 1, 233 | } 234 | result, err := client.Run(j) 235 | if err != nil { 236 | t.Fatalf("Response mismatch, err=%s", err) 237 | } 238 | 239 | if !result.Success { 240 | t.Fatalf("Success mismatch") 241 | } 242 | 243 | if !bytes.Equal([]byte("a"), result.Result) { 244 | t.Fatalf("Result mismatch") 245 | } 246 | 247 | expWrite := []byte( 248 | "run 6ba7b810-9dad-11d1-80b4-00c04fd430c4 j1 5000 1000 1 -priority=1\r\na\r\n", 249 | ) 250 | if !bytes.Equal(expWrite, conn.wrt.Bytes()) { 251 | t.Fatalf("Write mismatch, act=%s", conn.wrt.Bytes()) 252 | } 253 | } 254 | 255 | func TestRunErrors(t *testing.T) { 256 | tests := []RespErrTestCase{ 257 | { 258 | resp: []byte("-CLIENT-ERROR Invalid Job ID\r\n"), 259 | expErr: errors.New("CLIENT-ERROR Invalid Job ID"), 260 | }, 261 | } 262 | tests = append(tests, invalidCommonErrorTests()...) 263 | tests = append(tests, invalidResultErrorTests()...) 264 | 265 | for _, tt := range tests { 266 | conn := &TestConn{ 267 | rdr: bytes.NewBuffer(tt.resp), 268 | wrt: bytes.NewBuffer([]byte("")), 269 | } 270 | client := NewClient(conn) 271 | j := &FgJob{ 272 | ID: "6ba7b810-9dad-11d1-80b4-00c04fd430c4", 273 | Name: "j1", 274 | TTR: 5000, 275 | Timeout: 1000, 276 | Payload: []byte("a"), 277 | } 278 | result, err := client.Run(j) 279 | if result != nil || err == nil || tt.expErr == nil || err.Error() != tt.expErr.Error() { 280 | t.Fatalf("Response mismatch, result=%v, err=%q", result, err) 281 | } 282 | 283 | expWrite := []byte( 284 | "run 6ba7b810-9dad-11d1-80b4-00c04fd430c4 j1 5000 1000 1\r\na\r\n", 285 | ) 286 | if !bytes.Equal(expWrite, conn.wrt.Bytes()) { 287 | t.Fatalf("Write mismatch, act=%s", conn.wrt.Bytes()) 288 | } 289 | } 290 | } 291 | 292 | func TestRunBadConnError(t *testing.T) { 293 | conn := &TestBadWriteConn{} 294 | client := NewClient(conn) 295 | j := &FgJob{} 296 | result, err := client.Run(j) 297 | if _, ok := err.(*NetError); !ok { 298 | t.Fatalf("Error mismatch, err=%+v", err) 299 | } 300 | 301 | if result != nil { 302 | t.Fatalf("Response mismatch, resp=%+v", result) 303 | } 304 | } 305 | 306 | func TestSchedule(t *testing.T) { 307 | conn := &TestConn{ 308 | rdr: bytes.NewBuffer([]byte("+OK\r\n")), 309 | wrt: bytes.NewBuffer([]byte("")), 310 | } 311 | client := NewClient(conn) 312 | j := &ScheduledJob{ 313 | ID: "6ba7b810-9dad-11d1-80b4-00c04fd430c4", 314 | Name: "j1", 315 | TTR: 5000, 316 | TTL: 60000, 317 | Time: "2016-01-02T15:04:05Z", 318 | Payload: []byte("a"), 319 | } 320 | err := client.Schedule(j) 321 | if err != nil { 322 | t.Fatalf("Response mismatch, err=%s", err) 323 | } 324 | 325 | expWrite := []byte( 326 | "schedule 6ba7b810-9dad-11d1-80b4-00c04fd430c4 j1 5000 60000 2016-01-02T15:04:05Z 1\r\na\r\n", 327 | ) 328 | if !bytes.Equal(expWrite, conn.wrt.Bytes()) { 329 | t.Fatalf("Write mismatch, act=%s", conn.wrt.Bytes()) 330 | } 331 | } 332 | 333 | func TestScheduleOptionalFlags(t *testing.T) { 334 | tests := []struct { 335 | job *ScheduledJob 336 | expWrite []byte 337 | }{ 338 | { 339 | job: &ScheduledJob{ 340 | ID: "6ba7b810-9dad-11d1-80b4-00c04fd430c4", 341 | Name: "j1", 342 | TTR: 1, 343 | TTL: 2, 344 | Time: "2016-12-01T00:00:00Z", 345 | Payload: []byte(""), 346 | Priority: 100, 347 | }, 348 | expWrite: []byte( 349 | "schedule 6ba7b810-9dad-11d1-80b4-00c04fd430c4 j1 1 2 2016-12-01T00:00:00Z 0 -priority=100\r\n\r\n", 350 | ), 351 | }, 352 | { 353 | job: &ScheduledJob{ 354 | ID: "6ba7b810-9dad-11d1-80b4-00c04fd430c4", 355 | Name: "j1", 356 | TTR: 1, 357 | TTL: 2, 358 | Time: "2016-12-01T00:00:00Z", 359 | Payload: []byte(""), 360 | MaxAttempts: 3, 361 | }, 362 | expWrite: []byte( 363 | "schedule 6ba7b810-9dad-11d1-80b4-00c04fd430c4 j1 1 2 2016-12-01T00:00:00Z 0 -max-attempts=3\r\n\r\n", 364 | ), 365 | }, 366 | { 367 | job: &ScheduledJob{ 368 | ID: "6ba7b810-9dad-11d1-80b4-00c04fd430c4", 369 | Name: "j1", 370 | TTR: 1, 371 | TTL: 2, 372 | Time: "2016-12-01T00:00:00Z", 373 | Payload: []byte(""), 374 | MaxFails: 3, 375 | }, 376 | expWrite: []byte( 377 | "schedule 6ba7b810-9dad-11d1-80b4-00c04fd430c4 j1 1 2 2016-12-01T00:00:00Z 0 -max-fails=3\r\n\r\n", 378 | ), 379 | }, 380 | { 381 | job: &ScheduledJob{ 382 | ID: "6ba7b810-9dad-11d1-80b4-00c04fd430c4", 383 | Name: "j1", 384 | TTR: 1, 385 | TTL: 2, 386 | Time: "2016-12-01T00:00:00Z", 387 | Payload: []byte(""), 388 | Priority: 1, 389 | MaxAttempts: 3, 390 | MaxFails: 1, 391 | }, 392 | expWrite: []byte( 393 | "schedule 6ba7b810-9dad-11d1-80b4-00c04fd430c4 j1 1 2 2016-12-01T00:00:00Z 0 -priority=1 -max-attempts=3 -max-fails=1\r\n\r\n", 394 | ), 395 | }, 396 | } 397 | 398 | for _, tt := range tests { 399 | conn := &TestConn{ 400 | rdr: bytes.NewBuffer([]byte("+OK\r\n")), 401 | wrt: bytes.NewBuffer([]byte("")), 402 | } 403 | client := NewClient(conn) 404 | err := client.Schedule(tt.job) 405 | if err != nil { 406 | t.Fatalf("Response mismatch, err=%s", err) 407 | } 408 | 409 | if !bytes.Equal(tt.expWrite, conn.wrt.Bytes()) { 410 | t.Fatalf("Write mismatch, act=%q", conn.wrt.Bytes()) 411 | } 412 | } 413 | } 414 | 415 | func TestScheduleErrors(t *testing.T) { 416 | tests := []RespErrTestCase{ 417 | { 418 | resp: []byte("-CLIENT-ERROR Invalid Job ID\r\n"), 419 | expErr: errors.New("CLIENT-ERROR Invalid Job ID"), 420 | }, 421 | } 422 | 423 | for _, tt := range tests { 424 | conn := &TestConn{ 425 | rdr: bytes.NewBuffer(tt.resp), 426 | wrt: bytes.NewBuffer([]byte("")), 427 | } 428 | client := NewClient(conn) 429 | j := &ScheduledJob{} 430 | err := client.Schedule(j) 431 | if err == nil || tt.expErr == nil || err.Error() != tt.expErr.Error() { 432 | t.Fatalf("Response mismatch, err=%q", err) 433 | } 434 | } 435 | } 436 | 437 | func TestScheduleBaddConnError(t *testing.T) { 438 | conn := &TestBadWriteConn{} 439 | client := NewClient(conn) 440 | j := &ScheduledJob{} 441 | err := client.Schedule(j) 442 | if _, ok := err.(*NetError); !ok { 443 | t.Fatalf("Error mismatch, err=%+v", err) 444 | } 445 | } 446 | 447 | func TestResult(t *testing.T) { 448 | conn := &TestConn{ 449 | rdr: bytes.NewBuffer([]byte( 450 | "+OK 1\r\n" + 451 | "6ba7b810-9dad-11d1-80b4-00c04fd430c4 1 1\r\n" + 452 | "a\r\n", 453 | )), 454 | wrt: bytes.NewBuffer([]byte("")), 455 | } 456 | client := NewClient(conn) 457 | result, err := client.Result("6ba7b810-9dad-11d1-80b4-00c04fd430c4", 1000) 458 | if err != nil { 459 | t.Fatalf("Response mismatch, err=%s", err) 460 | } 461 | 462 | if !result.Success { 463 | t.Fatalf("Success mismatch") 464 | } 465 | 466 | if !bytes.Equal([]byte("a"), result.Result) { 467 | t.Fatalf("Resullt mismatch") 468 | } 469 | 470 | expWrite := []byte( 471 | "result 6ba7b810-9dad-11d1-80b4-00c04fd430c4 1000\r\n", 472 | ) 473 | if !bytes.Equal(expWrite, conn.wrt.Bytes()) { 474 | t.Fatalf("Write mismatch, act=%s", conn.wrt.Bytes()) 475 | } 476 | } 477 | 478 | func TestResultTimeout(t *testing.T) { 479 | conn := &TestConn{ 480 | rdr: bytes.NewBuffer([]byte( 481 | "-TIMED-OUT\r\n", 482 | )), 483 | wrt: bytes.NewBuffer([]byte("")), 484 | } 485 | client := NewClient(conn) 486 | if _, err := client.Result("6ba7b810-9dad-11d1-80b4-00c04fd430c4", 1000); err != nil { 487 | werr, ok := err.(*ResponseError) 488 | if !ok { 489 | t.Fatalf("Response mismatch, err=%s", err) 490 | } 491 | 492 | if werr.Code() != "TIMED-OUT" { 493 | t.Fatalf("Response mismatch, err=%s", err) 494 | } 495 | } 496 | 497 | expWrite := []byte( 498 | "result 6ba7b810-9dad-11d1-80b4-00c04fd430c4 1000\r\n", 499 | ) 500 | 501 | if !bytes.Equal(expWrite, conn.wrt.Bytes()) { 502 | t.Fatalf("Write mismatch, act=%s", conn.wrt.Bytes()) 503 | } 504 | } 505 | 506 | func TestResultErrors(t *testing.T) { 507 | var tests []RespErrTestCase 508 | tests = append(tests, invalidResultErrorTests()...) 509 | tests = append(tests, invalidCommonErrorTests()...) 510 | for _, tt := range tests { 511 | conn := &TestConn{ 512 | rdr: bytes.NewBuffer(tt.resp), 513 | wrt: bytes.NewBuffer([]byte("")), 514 | } 515 | client := NewClient(conn) 516 | result, err := client.Result("6ba7b810-9dad-11d1-80b4-00c04fd430c4", 1000) 517 | if result != nil || err == nil || tt.expErr == nil || err.Error() != tt.expErr.Error() { 518 | t.Fatalf("Response mismatch, err=%q, expErr=%q", err, tt.expErr) 519 | } 520 | } 521 | } 522 | 523 | func TestResultBadConnError(t *testing.T) { 524 | conn := &TestBadWriteConn{} 525 | client := NewClient(conn) 526 | result, err := client.Result("6ba7b810-9dad-11d1-80b4-00c04fd430c4", 1000) 527 | if _, ok := err.(*NetError); !ok { 528 | t.Fatalf("Error mismatch, err=%+v", err) 529 | } 530 | 531 | if result != nil { 532 | t.Fatalf("Result mismatch, result=%+v", result) 533 | } 534 | } 535 | 536 | func TestLease(t *testing.T) { 537 | conn := &TestConn{ 538 | rdr: bytes.NewBuffer([]byte( 539 | "+OK 1\r\n" + 540 | "6ba7b810-9dad-11d1-80b4-00c04fd430c4 j1 1000 1\r\n" + 541 | "a\r\n", 542 | )), 543 | wrt: bytes.NewBuffer([]byte("")), 544 | } 545 | client := NewClient(conn) 546 | j, err := client.Lease([]string{"j1"}, 1000) 547 | if err != nil { 548 | t.Fatalf("Response mismatch, err=%s", err) 549 | } 550 | 551 | if j.ID != "6ba7b810-9dad-11d1-80b4-00c04fd430c4" { 552 | t.Fatalf("ID mismatch") 553 | } 554 | 555 | if j.Name != "j1" { 556 | t.Fatalf("Name mismatch") 557 | } 558 | 559 | if j.TTR != 1000 { 560 | t.Fatalf("TTR mismatch") 561 | } 562 | 563 | if !bytes.Equal([]byte("a"), j.Payload) { 564 | t.Fatalf("Payload mismatch") 565 | } 566 | 567 | expWrite := []byte( 568 | "lease j1 1000\r\n", 569 | ) 570 | if !bytes.Equal(expWrite, conn.wrt.Bytes()) { 571 | t.Fatalf("Write mismatch, act=%s", conn.wrt.Bytes()) 572 | } 573 | } 574 | 575 | func TestLeaseErrors(t *testing.T) { 576 | tests := []RespErrTestCase{ 577 | // Invalid reply-count 578 | { 579 | resp: []byte( 580 | "+OK 2\r\n" + 581 | "6ba7b810-9dad-11d1-80b4-00c04fd430c4 j1 1\r\n" + 582 | "a\r\n", 583 | ), 584 | expErr: ErrMalformed, 585 | }, 586 | // Space after reply-count 587 | { 588 | resp: []byte( 589 | "+OK 1 \r\n" + 590 | "6ba7b810-9dad-11d1-80b4-00c04fd430c4 j1 1\r\n" + 591 | "a\r\n", 592 | ), 593 | expErr: ErrMalformed, 594 | }, 595 | // Whitespace as reply-count 596 | { 597 | resp: []byte( 598 | "+OK \r\n" + 599 | "6ba7b810-9dad-11d1-80b4-00c04fd430c4 j1 1\r\n" + 600 | "a\r\n", 601 | ), 602 | expErr: ErrMalformed, 603 | }, 604 | // Missing ID 605 | { 606 | resp: []byte( 607 | "+OK 1\r\n" + 608 | "j1 1\r\n" + 609 | "a\r\n", 610 | ), 611 | expErr: ErrMalformed, 612 | }, 613 | // Invalid ID 614 | { 615 | resp: []byte( 616 | "+OK 1\r\n" + 617 | "* j1 1\r\n" + 618 | "a\r\n", 619 | ), 620 | expErr: ErrMalformed, 621 | }, 622 | // Invalid name 623 | { 624 | resp: []byte( 625 | "+OK 1\r\n" + 626 | "6ba7b810-9dad-11d1-80b4-00c04fd430c4 * 1\r\n" + 627 | "a\r\n", 628 | ), 629 | expErr: ErrMalformed, 630 | }, 631 | // Invalid size 632 | { 633 | resp: []byte( 634 | "+OK 1\r\n" + 635 | "6ba7b810-9dad-11d1-80b4-00c04fd430c4 j1 *\r\n" + 636 | "a\r\n", 637 | ), 638 | expErr: ErrMalformed, 639 | }, 640 | // Missing job payload 641 | { 642 | resp: []byte( 643 | "+OK 1\r\n" + 644 | "6ba7b810-9dad-11d1-80b4-00c04fd430c4 j1 1\r\n" + 645 | "\r\n", 646 | ), 647 | expErr: ErrMalformed, 648 | }, 649 | // Missing job payload with size greater than payload + \r\n 650 | // Triggers incomplete response read. 651 | { 652 | resp: []byte( 653 | "+OK 1\r\n" + 654 | "6ba7b810-9dad-11d1-80b4-00c04fd430c4 j1 10\r\n" + 655 | "\r\n", 656 | ), 657 | expErr: ErrMalformed, 658 | }, 659 | } 660 | tests = append(tests, invalidCommonErrorTests()...) 661 | 662 | for _, tt := range tests { 663 | conn := &TestConn{ 664 | rdr: bytes.NewBuffer(tt.resp), 665 | wrt: bytes.NewBuffer([]byte("")), 666 | } 667 | client := NewClient(conn) 668 | j, err := client.Lease([]string{"j1"}, 1000) 669 | if j != nil || err == nil || tt.expErr == nil || err.Error() != tt.expErr.Error() { 670 | t.Fatalf("Response mismatch, err=%q, expErr=%q", err, tt.expErr) 671 | } 672 | } 673 | } 674 | 675 | func TestLeaseBadConnError(t *testing.T) { 676 | conn := &TestBadWriteConn{} 677 | client := NewClient(conn) 678 | j, err := client.Lease([]string{"j1"}, 1000) 679 | if _, ok := err.(*NetError); !ok { 680 | t.Fatalf("Error mismatch, err=%+v", err) 681 | } 682 | 683 | if j != nil { 684 | t.Fatalf("Response mismatch, job=%+v", j) 685 | } 686 | } 687 | 688 | func TestComplete(t *testing.T) { 689 | conn := &TestConn{ 690 | rdr: bytes.NewBuffer([]byte("+OK\r\n")), 691 | wrt: bytes.NewBuffer([]byte("")), 692 | } 693 | client := NewClient(conn) 694 | err := client.Complete("6ba7b810-9dad-11d1-80b4-00c04fd430c4", []byte("a")) 695 | if err != nil { 696 | t.Fatalf("Response mismatch, err=%s", err) 697 | } 698 | 699 | expWrite := []byte( 700 | "complete 6ba7b810-9dad-11d1-80b4-00c04fd430c4 1\r\na\r\n", 701 | ) 702 | if !bytes.Equal(expWrite, conn.wrt.Bytes()) { 703 | t.Fatalf("Write mismatch, act=%s", conn.wrt.Bytes()) 704 | } 705 | } 706 | 707 | func TestCompleteErrors(t *testing.T) { 708 | var tests []RespErrTestCase 709 | tests = append(tests, invalidCommonErrorTests()...) 710 | 711 | for _, tt := range tests { 712 | conn := &TestConn{ 713 | rdr: bytes.NewBuffer(tt.resp), 714 | wrt: bytes.NewBuffer([]byte("")), 715 | } 716 | client := NewClient(conn) 717 | err := client.Complete("6ba7b810-9dad-11d1-80b4-00c04fd430c4", []byte("a")) 718 | if err == nil || tt.expErr == nil || err.Error() != tt.expErr.Error() { 719 | t.Fatalf("Response mismatch, err=%q, expErr=%q", err, tt.expErr) 720 | } 721 | } 722 | } 723 | 724 | func TestCompleteBadConnError(t *testing.T) { 725 | conn := &TestBadWriteConn{} 726 | client := NewClient(conn) 727 | err := client.Complete("6ba7b810-9dad-11d1-80b4-00c04fd430c4", []byte("a")) 728 | if _, ok := err.(*NetError); !ok { 729 | t.Fatalf("Error mismatch, err=%+v", err) 730 | } 731 | } 732 | 733 | func TestFail(t *testing.T) { 734 | conn := &TestConn{ 735 | rdr: bytes.NewBuffer([]byte("+OK\r\n")), 736 | wrt: bytes.NewBuffer([]byte("")), 737 | } 738 | client := NewClient(conn) 739 | err := client.Fail("6ba7b810-9dad-11d1-80b4-00c04fd430c4", []byte("a")) 740 | if err != nil { 741 | t.Fatalf("Response mismatch, err=%s", err) 742 | } 743 | 744 | expWrite := []byte( 745 | "fail 6ba7b810-9dad-11d1-80b4-00c04fd430c4 1\r\na\r\n", 746 | ) 747 | if !bytes.Equal(expWrite, conn.wrt.Bytes()) { 748 | t.Fatalf("Write mismatch, act=%s", conn.wrt.Bytes()) 749 | } 750 | } 751 | 752 | func TestFailErrors(t *testing.T) { 753 | var tests []RespErrTestCase 754 | tests = append(tests, invalidCommonErrorTests()...) 755 | 756 | for _, tt := range tests { 757 | conn := &TestConn{ 758 | rdr: bytes.NewBuffer(tt.resp), 759 | wrt: bytes.NewBuffer([]byte("")), 760 | } 761 | client := NewClient(conn) 762 | err := client.Fail("6ba7b810-9dad-11d1-80b4-00c04fd430c4", []byte("a")) 763 | if err == nil || tt.expErr == nil || err.Error() != tt.expErr.Error() { 764 | t.Fatalf("Response mismatch, err=%q, expErr=%q", err, tt.expErr) 765 | } 766 | } 767 | } 768 | 769 | func TestFailBadConnError(t *testing.T) { 770 | conn := &TestBadWriteConn{} 771 | client := NewClient(conn) 772 | err := client.Fail("6ba7b810-9dad-11d1-80b4-00c04fd430c4", []byte("a")) 773 | if _, ok := err.(*NetError); !ok { 774 | t.Fatalf("Error mismatch, err=%+v", err) 775 | } 776 | } 777 | 778 | func TestDelete(t *testing.T) { 779 | conn := &TestConn{ 780 | rdr: bytes.NewBuffer([]byte("+OK\r\n")), 781 | wrt: bytes.NewBuffer([]byte("")), 782 | } 783 | client := NewClient(conn) 784 | err := client.Delete("6ba7b810-9dad-11d1-80b4-00c04fd430c4") 785 | if err != nil { 786 | t.Fatalf("Response mismatch, err=%s", err) 787 | } 788 | 789 | expWrite := []byte( 790 | "delete 6ba7b810-9dad-11d1-80b4-00c04fd430c4\r\n", 791 | ) 792 | if !bytes.Equal(expWrite, conn.wrt.Bytes()) { 793 | t.Fatalf("Write mismatch, act=%s", conn.wrt.Bytes()) 794 | } 795 | } 796 | 797 | func TestDeleteErrors(t *testing.T) { 798 | var tests []RespErrTestCase 799 | tests = append(tests, invalidCommonErrorTests()...) 800 | 801 | for _, tt := range tests { 802 | conn := &TestConn{ 803 | rdr: bytes.NewBuffer(tt.resp), 804 | wrt: bytes.NewBuffer([]byte("")), 805 | } 806 | client := NewClient(conn) 807 | err := client.Delete("6ba7b810-9dad-11d1-80b4-00c04fd430c4") 808 | if err == nil || tt.expErr == nil || err.Error() != tt.expErr.Error() { 809 | t.Fatalf("Response mismatch, err=%q, expErr=%q", err, tt.expErr) 810 | } 811 | } 812 | } 813 | 814 | func TestDeleteBadConnError(t *testing.T) { 815 | conn := &TestBadWriteConn{} 816 | client := NewClient(conn) 817 | err := client.Delete("6ba7b810-9dad-11d1-80b4-00c04fd430c4") 818 | if _, ok := err.(*NetError); !ok { 819 | t.Fatalf("Error mismatch, err=%+v", err) 820 | } 821 | } 822 | 823 | type RespErrTestCase struct { 824 | resp []byte 825 | expErr error 826 | } 827 | 828 | func invalidCommonErrorTests() []RespErrTestCase { 829 | return []RespErrTestCase{ 830 | { 831 | resp: []byte(""), 832 | expErr: NewNetError("EOF"), 833 | }, 834 | { 835 | resp: []byte("*OK\r\n"), 836 | expErr: ErrMalformed, 837 | }, 838 | { 839 | resp: []byte("-NOT-FOUND"), 840 | expErr: NewNetError("EOF"), 841 | }, 842 | // Whitespace as code and text 843 | { 844 | resp: []byte("- \r\n"), 845 | expErr: ErrMalformed, 846 | }, 847 | // Whitespace as code 848 | { 849 | resp: []byte("- \r\n"), 850 | expErr: ErrMalformed, 851 | }, 852 | // Whitespace as error text. 853 | { 854 | resp: []byte("-C \r\n"), 855 | expErr: ErrMalformed, 856 | }, 857 | { 858 | resp: []byte("\n"), 859 | expErr: ErrMalformed, 860 | }, 861 | { 862 | resp: []byte("a\n"), 863 | expErr: ErrMalformed, 864 | }, 865 | { 866 | resp: []byte("\r\n"), 867 | expErr: ErrMalformed, 868 | }, 869 | { 870 | resp: []byte("NOT-FOUND\r\n"), 871 | expErr: ErrMalformed, 872 | }, 873 | { 874 | resp: []byte("NOT-FOUND"), 875 | expErr: NewNetError("EOF"), 876 | }, 877 | { 878 | resp: []byte("-NOT-FOUND\r\n"), 879 | expErr: NewResponseError("NOT-FOUND", ""), 880 | }, 881 | { 882 | resp: []byte("-TIMED-OUT\r\n"), 883 | expErr: NewResponseError("TIMED-OUT", ""), 884 | }, 885 | } 886 | } 887 | 888 | func invalidResultErrorTests() []RespErrTestCase { 889 | return []RespErrTestCase{ 890 | // Invalid reply-count 891 | { 892 | resp: []byte("+OK 2\r\n6ba7b810-9dad-11d1-80b4-00c04fd430c4 0 1\r\na\r\n"), 893 | expErr: ErrMalformed, 894 | }, 895 | // Missing result data 896 | { 897 | resp: []byte("+OK 1\r\n6ba7b810-9dad-11d1-80b4-00c04fd430c4 0 1\r\n\r\n"), 898 | expErr: ErrMalformed, 899 | }, 900 | // Missing result data with result-size greater than expected result + \r\n 901 | // Triggers incomplete read. 902 | { 903 | resp: []byte("+OK 1\r\n6ba7b810-9dad-11d1-80b4-00c04fd430c4 0 10\r\n\r\n"), 904 | expErr: ErrMalformed, 905 | }, 906 | { 907 | resp: []byte("+OK 2\r\n" + 908 | "6ba7b810-9dad-11d1-80b4-00c04fd430c4 1 1\r\n" + 909 | "a\r\n"), 910 | expErr: ErrMalformed, 911 | }, 912 | { 913 | resp: []byte("+OK 1\r\n" + 914 | "6ba7b810-9dad-11d1-80b4-00c04fd430c4 1 1 1\r\n" + 915 | "a\r\n"), 916 | expErr: ErrMalformed, 917 | }, 918 | { 919 | resp: []byte("+OK 1\r\n" + 920 | "6ba7b810-9dad-11d1-80b4-00c04fd430c4 1 -1\r\n" + 921 | "a\r\n"), 922 | expErr: ErrMalformed, 923 | }, 924 | { 925 | resp: []byte("+OK 1\r\n" + 926 | "6ba7b810-9dad-11d1-80b4-00c04fd430c4 1 1048577\r\n" + 927 | "a\r\n"), 928 | expErr: ErrMalformed, 929 | }, 930 | { 931 | resp: []byte("+OK 1\r\n" + 932 | "6ba7b810-9dad-11d1-80b4-00c04fd430c4 -1 1\r\n" + 933 | "a\r\n"), 934 | expErr: ErrMalformed, 935 | }, 936 | } 937 | } 938 | 939 | type TestConn struct { 940 | rdr *bytes.Buffer 941 | wrt *bytes.Buffer 942 | proxyConn net.Conn 943 | } 944 | 945 | func (c *TestConn) Read(b []byte) (int, error) { 946 | return c.rdr.Read(b) 947 | } 948 | 949 | func (c *TestConn) Write(b []byte) (int, error) { 950 | return c.wrt.Write(b) 951 | } 952 | 953 | func (c *TestConn) Close() error { 954 | return nil 955 | } 956 | 957 | func (c *TestConn) SetDeadline(t time.Time) error { 958 | return nil 959 | } 960 | 961 | func (c *TestConn) SetReadDeadline(t time.Time) error { 962 | return nil 963 | } 964 | 965 | func (c *TestConn) SetWriteDeadline(t time.Time) error { 966 | return nil 967 | } 968 | 969 | func (c *TestConn) LocalAddr() net.Addr { 970 | return &TestAddr{} 971 | } 972 | 973 | func (c *TestConn) RemoteAddr() net.Addr { 974 | return &TestAddr{} 975 | } 976 | 977 | type TestAddr struct{} 978 | 979 | func (a *TestAddr) Network() string { 980 | return "" 981 | } 982 | 983 | func (a *TestAddr) String() string { 984 | return "" 985 | } 986 | 987 | type TestBadWriteConn struct { 988 | } 989 | 990 | func (c *TestBadWriteConn) Read(b []byte) (int, error) { 991 | return 0, nil 992 | } 993 | 994 | func (c *TestBadWriteConn) Write(b []byte) (int, error) { 995 | return 0, errors.New("A bad time") 996 | } 997 | 998 | func (c *TestBadWriteConn) Close() error { 999 | return nil 1000 | } 1001 | 1002 | func (c *TestBadWriteConn) SetDeadline(t time.Time) error { 1003 | return nil 1004 | } 1005 | 1006 | func (c *TestBadWriteConn) SetReadDeadline(t time.Time) error { 1007 | return nil 1008 | } 1009 | 1010 | func (c *TestBadWriteConn) SetWriteDeadline(t time.Time) error { 1011 | return nil 1012 | } 1013 | 1014 | func (c *TestBadWriteConn) LocalAddr() net.Addr { 1015 | return &TestAddr{} 1016 | } 1017 | 1018 | func (c *TestBadWriteConn) RemoteAddr() net.Addr { 1019 | return &TestAddr{} 1020 | } 1021 | --------------------------------------------------------------------------------