pax_global_header00006660000000000000000000000064145111453070014513gustar00rootroot0000000000000052 comment=f876e9de7e1935d3f78a472f4a8314c60edb6777 golang-github-rafaeljusto-redigomock-3.1.2/000077500000000000000000000000001451114530700207035ustar00rootroot00000000000000golang-github-rafaeljusto-redigomock-3.1.2/.gitignore000066400000000000000000000004031451114530700226700ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test golang-github-rafaeljusto-redigomock-3.1.2/.travis.yml000066400000000000000000000003341451114530700230140ustar00rootroot00000000000000language: go go: - tip install: - go get github.com/gomodule/redigo/redis script: - go test -race notifications: email: recipients: - adm@rafael.net.br on_success: change on_failure: always golang-github-rafaeljusto-redigomock-3.1.2/AUTHORS000066400000000000000000000000441451114530700217510ustar00rootroot00000000000000Rafael Dantas Justo - @rafaeljusto golang-github-rafaeljusto-redigomock-3.1.2/CONTRIBUTORS000066400000000000000000000006501451114530700225640ustar00rootroot00000000000000Ahmad Muzakki - @ahmadmuzakki29 Artem Krylysov - @akrylysov Ben Kraft - @benjaminjkraft Charles Law - @clawconduce Damien Mathieu - @dmathieu Drew Landis - @drewlandis Erik Davidson - @aphistic Jakob Green - @JakobGreen Maciej Galkowski - @szank Merlin Cox - @merlincox Mitch - @mitchsw Štěpán Pilař - @mingan Zachery Moneypenny - @whazzmastergolang-github-rafaeljusto-redigomock-3.1.2/Changelog000066400000000000000000000037601451114530700225230ustar00rootroot00000000000000# Change Log All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). ## [3.1.1] - 2022-04-05 ### Fix - Upgrade redigo dependency version to solve security warning - Improve code fixing linter warnings ## [3.1.0] - 2022-04-04 ### Added - Implement ConnWithContext interface ## [3.0.1] - 2020-12-17 ### Fix - Use a valid redigo dependency ## [3.0.0] - 2020-11-16 ### Fix - Refactoring to solve race conditions ## [2.4.0] - 2020-06-08 ### Added - Dynamic handle Redis arguments with the command handle feature ## [2.3.0] - 2020-02-19 ### Added - `ExpectPanic` to support expectations of panics instead of responses or errors ## [2.2.1] - 2019-11-17 ### Added - `FlushSkippableMock` should allow `Flush` to continue its processing when the mock return a nil ## [2.2.0] - 2019-02-02 ### Added - ExpectStringSlice to simplify expecting a slice of strings ### Fix - `Do` command with `nil` argument panics `implementsFuzzy` - `Flush` should process the queue of `Send` commands - `Conn` should satisfy `redis.Conn` and `redis.ConnWithTimeout` - Typos ### Changed - Using `gomodule/redigo` instead of `garyburd/redigo` ## [2.1.0] - 2017-07-20 ### Added - New ExpectSlice helper method - Detect if all expectations were met with AllCommandsCalled method ### Fix - Reset stats on Clear call - Documentation grammar problems - Safety check while acessing responses ## [2.0.0] - 2016-05-24 ### Added - Fuzzy matching for redigomock command arguments - Make commands a property of a connection object, which allows to run tests in parallel - Commands calls counters, which allows to identify unused mocked commands (thanks to @rylnd) ### Changed - Improve error message adding argument suggestions ## [1.0.0] - 2015-04-23 ### Added - Support to mock commands taking into account the arguments or not - Support to mock PubSub using a wait Go channel - Support to multiple (sequentially returned) responses for single command - Support to mock scripts golang-github-rafaeljusto-redigomock-3.1.2/LICENSE000066400000000000000000000020641451114530700217120ustar00rootroot00000000000000MIT License Copyright (c) 2023 Rafael Dantas Justo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-github-rafaeljusto-redigomock-3.1.2/README.md000066400000000000000000000110701451114530700221610ustar00rootroot00000000000000redigomock ========== [![Build Status](https://travis-ci.org/rafaeljusto/redigomock.png?branch=master)](https://travis-ci.org/rafaeljusto/redigomock) [![GoDoc](https://godoc.org/github.com/rafaeljusto/redigomock?status.png)](https://godoc.org/github.com/rafaeljusto/redigomock) Easy way to unit test projects using [redigo library](https://github.com/gomodule/redigo) (Redis client in go). You can find the latest release [here](https://github.com/rafaeljusto/redigomock/releases). install ------- ``` go get -u github.com/rafaeljusto/redigomock/v3 ``` usage ----- Here is an example of using redigomock, for more information please check the [API documentation](https://godoc.org/github.com/rafaeljusto/redigomock). ```go package main import ( "fmt" "github.com/gomodule/redigo/redis" "github.com/rafaeljusto/redigomock/v3" ) type Person struct { Name string `redis:"name"` Age int `redis:"age"` } func RetrievePerson(conn redis.Conn, id string) (Person, error) { var person Person values, err := redis.Values(conn.Do("HGETALL", fmt.Sprintf("person:%s", id))) if err != nil { return person, err } err = redis.ScanStruct(values, &person) return person, err } func main() { // Simulate command result conn := redigomock.NewConn() cmd := conn.Command("HGETALL", "person:1").ExpectMap(map[string]string{ "name": "Mr. Johson", "age": "42", }) person, err := RetrievePerson(conn, "1") if err != nil { fmt.Println(err) return } if conn.Stats(cmd) != 1 { fmt.Println("Command was not used") return } if person.Name != "Mr. Johson" { fmt.Printf("Invalid name. Expected 'Mr. Johson' and got '%s'\n", person.Name) return } if person.Age != 42 { fmt.Printf("Invalid age. Expected '42' and got '%d'\n", person.Age) return } // Simulate command error conn.Clear() cmd = conn.Command("HGETALL", "person:1").ExpectError(fmt.Errorf("Simulate error!")) person, err = RetrievePerson(conn, "1") if err == nil { fmt.Println("Should return an error!") return } if conn.Stats(cmd) != 1 { fmt.Println("Command was not used") return } fmt.Println("Success!") } ``` mocking a subscription ---------------------- ```go package main import "github.com/rafaeljusto/redigomock/v3" func CreateSubscriptionMessage(data []byte) []interface{} { values := []interface{}{} values = append(values, interface{}([]byte("message"))) values = append(values, interface{}([]byte("chanName"))) values = append(values, interface{}(data)) return values } func main() { conn := redigomock.NewConn() // Setup the initial subscription message values := []interface{}{} values = append(values, interface{}([]byte("subscribe"))) values = append(values, interface{}([]byte("chanName"))) values = append(values, interface{}([]byte("1"))) conn.Command("SUBSCRIBE", subKey).Expect(values) conn.ReceiveWait = true // Add a response that will come back as a subscription message conn.AddSubscriptionMessage(CreateSubscriptionMessage([]byte("hello"))) // You need to send messages to conn.ReceiveNow in order to get a response. // Sending to this channel will block until receive, so do it in a goroutine go func() { conn.ReceiveNow <- true // This unlocks the subscribe message conn.ReceiveNow <- true // This sends the "hello" message }() } ``` connections pool ---------------- ```go // Note you cannot get access to the connection via the pool, // the only way is to use this conn variable. conn := redigomock.NewConn() pool := &redis.Pool{ // Return the same connection mock for each Get() call. Dial: func() (redis.Conn, error) { return conn, nil }, MaxIdle: 10, } ``` dynamic handling arguments -------------------------- Sometimes you need to check the executed arguments in your Redis command. For that you can use the command handler. ```go package main import ( "fmt" "github.com/gomodule/redigo/redis" "github.com/rafaeljusto/redigomock/v3" ) func Publish(conn redis.Conn, x, y int) error { if x < 0 { x = 0 } if y < 0 { y = 0 } _, err := conn.Do("PUBLISH", "sumCh", int64(x+y)) return err } func main() { conn := redigomock.NewConn() conn.GenericCommand("PUBLISH").Handle(redigomock.ResponseHandler(func(args []interface{}) (interface{}, error) {{ if len(args) != 2 { return nil, fmt.Errorf("unexpected number of arguments: %d", len(args)) } v, ok := args[1].(int64) if !ok { return nil, fmt.Errorf("unexpected type %T", args[1]) } if v < 0 { return nil, fmt.Errorf("unexpected value '%d'", v) } return int64(1), nil }) if err := Publish(conn, -1, 10); err != nil { fmt.Println(err) return } fmt.Println("Success!") } ```golang-github-rafaeljusto-redigomock-3.1.2/command.go000066400000000000000000000117561451114530700226620ustar00rootroot00000000000000// Copyright 2014 Rafael Dantas Justo. All rights reserved. // Use of this source code is governed by a GPL // license that can be found in the LICENSE file. package redigomock import ( "fmt" "reflect" "sync" ) // response struct that represents single response from `Do` call. type response struct { response interface{} // Response to send back when this command/arguments are called err error // Error to send back when this command/arguments are called panicVal interface{} // Panic to throw when this command/arguments are called } // ResponseHandler dynamic handles the response for the provided arguments. type ResponseHandler func(args []interface{}) (interface{}, error) // Cmd stores the registered information about a command to return it later // when request by a command execution type Cmd struct { // name and args must not be mutated after creation. name string // Name of the command args []interface{} // Arguments of the command responses []response // Slice of returned responses called bool // State for this command called or not mu sync.Mutex // hold while accessing responses and called } // cmdHash stores a unique identifier of the command type cmdHash string // equal verify if a command/arguments is related to a registered command func equal(commandName string, args []interface{}, cmd *Cmd) bool { if commandName != cmd.name || len(args) != len(cmd.args) { return false } for pos := range cmd.args { if implementsFuzzy(cmd.args[pos]) && implementsFuzzy(args[pos]) { if reflect.TypeOf(cmd.args[pos]) != reflect.TypeOf(args[pos]) { return false } } else if implementsFuzzy(cmd.args[pos]) || implementsFuzzy(args[pos]) { return false } else { if !reflect.DeepEqual(cmd.args[pos], args[pos]) { return false } } } return true } // match check if provided arguments can be matched with any registered // commands func match(commandName string, args []interface{}, cmd *Cmd) bool { if commandName != cmd.name || len(args) != len(cmd.args) { return false } for pos := range cmd.args { if implementsFuzzy(cmd.args[pos]) { if !cmd.args[pos].(FuzzyMatcher).Match(args[pos]) { return false } } else if !reflect.DeepEqual(cmd.args[pos], args[pos]) { return false } } return true } // Expect sets a response for this command. Every time a Do or Receive method // is executed for a registered command this response or error will be // returned. Expect call returns a pointer to Cmd struct, so you can chain // Expect calls. Chained responses will be returned on subsequent calls // matching this commands arguments in FIFO order func (c *Cmd) Expect(resp interface{}) *Cmd { c.expect(response{resp, nil, nil}) return c } // expect appends the argument to the response-slice, holding the lock func (c *Cmd) expect(resp response) { c.mu.Lock() defer c.mu.Unlock() c.responses = append(c.responses, resp) } // ExpectMap works in the same way of the Expect command, but has a key/value // input to make it easier to build test environments func (c *Cmd) ExpectMap(resp map[string]string) *Cmd { var values []interface{} for key, value := range resp { values = append(values, []byte(key)) values = append(values, []byte(value)) } c.expect(response{values, nil, nil}) return c } // ExpectError allows you to force an error when executing a // command/arguments func (c *Cmd) ExpectError(err error) *Cmd { c.expect(response{nil, err, nil}) return c } // ExpectPanic allows you to force a panic when executing a // command/arguments func (c *Cmd) ExpectPanic(msg interface{}) *Cmd { c.expect(response{nil, nil, msg}) return c } // ExpectSlice makes it easier to expect slice value // e.g - HMGET command func (c *Cmd) ExpectSlice(resp ...interface{}) *Cmd { ifaces := []interface{}{} ifaces = append(ifaces, resp...) c.expect(response{ifaces, nil, nil}) return c } // ExpectStringSlice makes it easier to expect a slice of strings, plays nicely // with redigo.Strings func (c *Cmd) ExpectStringSlice(resp ...string) *Cmd { ifaces := []interface{}{} for _, r := range resp { ifaces = append(ifaces, []byte(r)) } c.expect(response{ifaces, nil, nil}) return c } // Handle registers a function to handle the incoming arguments, generating an // on-the-fly response. func (c *Cmd) Handle(fn ResponseHandler) *Cmd { c.expect(response{fn, nil, nil}) return c } // hash generates a unique identifier for the command func (c *Cmd) hash() cmdHash { output := c.name for _, arg := range c.args { output += fmt.Sprintf("%v", arg) } return cmdHash(output) } // Called returns true if the command-mock was ever called. func (c *Cmd) Called() bool { c.mu.Lock() defer c.mu.Unlock() return c.called } // getResponse marks the command as used, and gets the next response to return. func (c *Cmd) getResponse() *response { c.mu.Lock() defer c.mu.Unlock() c.called = true if len(c.responses) == 0 { return nil } resp := c.responses[0] if len(c.responses) > 1 { c.responses = c.responses[1:] } return &resp } golang-github-rafaeljusto-redigomock-3.1.2/command_test.go000066400000000000000000000351351451114530700237160ustar00rootroot00000000000000// Copyright 2014 Rafael Dantas Justo. All rights reserved. // Use of this source code is governed by a GPL // license that can be found in the LICENSE file. package redigomock import ( "crypto/sha1" "encoding/hex" "fmt" "math/rand" "sync" "testing" "github.com/gomodule/redigo/redis" ) func TestCommand(t *testing.T) { connection := NewConn() connection.Command("HGETALL", "a", "b", "c") if len(connection.commands) != 1 { t.Fatalf("Did not register the command. Expected '1' and got '%d'", len(connection.commands)) } cmd := connection.commands[0] if cmd.name != "HGETALL" { t.Error("Wrong name defined for command") } if len(cmd.args) != 3 { t.Fatal("Wrong arguments defined for command") } arg := cmd.args[0].(string) if arg != "a" { t.Errorf("Wrong argument defined for command. Expected 'a' and got '%s'", arg) } arg = cmd.args[1].(string) if arg != "b" { t.Errorf("Wrong argument defined for command. Expected 'b' and got '%s'", arg) } arg = cmd.args[2].(string) if arg != "c" { t.Errorf("Wrong argument defined for command. Expected 'c' and got '%s'", arg) } if len(cmd.responses) != 0 { t.Error("Response defined without any call") } } func TestScript(t *testing.T) { connection := NewConn() scriptData := []byte("This should be a lua script for redis") h := sha1.New() h.Write(scriptData) sha1sum := hex.EncodeToString(h.Sum(nil)) connection.Script(scriptData, 0) // 0 connection.Script(scriptData, 0, "value1") // 1 connection.Script(scriptData, 1, "key1") // 2 connection.Script(scriptData, 1, "key1", "value1") // 3 connection.Script(scriptData, 2, "key1", "key2", "value1") // 4 if len(connection.commands) != 5 { t.Fatalf("Did not register the commands. Expected '5' and got '%d'", len(connection.commands)) } if connection.commands[0].name != "EVALSHA" { t.Error("Wrong name defined for command") } if len(connection.commands[0].args) != 2 { t.Errorf("Wrong arguments defined for command %v", connection.commands[0].args) } if len(connection.commands[1].args) != 3 { t.Error("Wrong arguments defined for command") } if len(connection.commands[2].args) != 3 { t.Error("Wrong arguments defined for command") } if len(connection.commands[3].args) != 4 { t.Error("Wrong arguments defined for command") } if len(connection.commands[4].args) != 5 { t.Error("Wrong arguments defined for command") } // Script(scriptData, 0) arg := connection.commands[0].args[0].(string) if arg != sha1sum { t.Errorf("Wrong argument defined for command. Expected '%s' and got '%s'", sha1sum, arg) } argInt := connection.commands[0].args[1].(int) if argInt != 0 { t.Errorf("Wrong argument defined for command. Expected '0' and got '%v'", argInt) } // Script(scriptData, 0, "value1") argInt = connection.commands[1].args[1].(int) if argInt != 0 { t.Errorf("Wrong argument defined for command. Expected '0' and got '%v'", argInt) } arg = connection.commands[1].args[2].(string) if arg != "value1" { t.Errorf("Wrong argument defined for command. Expected 'value1' and got '%s'", arg) } // Script(scriptData, 1, "key1") argInt = connection.commands[2].args[1].(int) if argInt != 1 { t.Errorf("Wrong argument defined for command. Expected '1' and got '%v'", argInt) } arg = connection.commands[2].args[2].(string) if arg != "key1" { t.Errorf("Wrong argument defined for command. Expected 'key1' and got '%s'", arg) } // Script(scriptData, 1, "key1", "value1") argInt = connection.commands[3].args[1].(int) if argInt != 1 { t.Errorf("Wrong argument defined for command. Expected '1' and got '%v'", argInt) } arg = connection.commands[3].args[2].(string) if arg != "key1" { t.Errorf("Wrong argument defined for command. Expected 'key1' and got '%s'", arg) } arg = connection.commands[3].args[3].(string) if arg != "value1" { t.Errorf("Wrong argument defined for command. Expected 'value1' and got '%s'", arg) } // Script(scriptData, 2, "key1", "key2", "value1") argInt = connection.commands[4].args[1].(int) if argInt != 2 { t.Errorf("Wrong argument defined for command. Expected '2' and got '%v'", argInt) } arg = connection.commands[4].args[2].(string) if arg != "key1" { t.Errorf("Wrong argument defined for command. Expected 'key1' and got '%s'", arg) } arg = connection.commands[4].args[3].(string) if arg != "key2" { t.Errorf("Wrong argument defined for command. Expected 'key2' and got '%s'", arg) } arg = connection.commands[4].args[4].(string) if arg != "value1" { t.Errorf("Wrong argument defined for command. Expected 'value1' and got '%s'", arg) } } func TestGenericCommand(t *testing.T) { connection := NewConn() connection.GenericCommand("HGETALL") if len(connection.commands) != 1 { t.Fatalf("Did not registered the command. Expected '1' and got '%d'", len(connection.commands)) } cmd := connection.commands[0] if cmd.name != "HGETALL" { t.Error("Wrong name defined for command") } if len(cmd.args) > 0 { t.Error("Arguments defined for command when they shouldn't") } if len(cmd.responses) != 0 { t.Error("Response defined without any call") } } func TestExpect(t *testing.T) { connection := NewConn() connection.Command("HGETALL").Expect("test") if len(connection.commands) != 1 { t.Fatalf("Did not registered the command. Expected '1' and got '%d'", len(connection.commands)) } cmd := connection.commands[0] if cmd.responses[0].response == nil { t.Fatal("Response not defined") } value, ok := cmd.responses[0].response.(string) if !ok { t.Fatal("Not storing response in the correct type") } if value != "test" { t.Error("Wrong response content") } } func TestExpectMap(t *testing.T) { connection := NewConn() connection.Command("HGETALL").ExpectMap(map[string]string{ "key1": "value1", }) if len(connection.commands) != 1 { t.Fatalf("Did not registered the command. Expected '1' and got '%d'", len(connection.commands)) } cmd := connection.commands[0] if cmd.responses[0].response == nil { t.Fatal("Response not defined") } values, ok := cmd.responses[0].response.([]interface{}) if !ok { t.Fatal("Not storing response in the correct type") } expected := []string{"key1", "value1"} if len(values) != len(expected) { t.Fatal("Map values not stored properly") } for i := 0; i < len(expected); i++ { value, ok := values[i].([]byte) if ok { if string(value) != expected[i] { t.Errorf("Changing the response content. Expected '%s' and got '%s'", expected[i], string(value)) } } else { t.Error("Not storing the map content in byte format") } } } func TestExpectMapReplace(t *testing.T) { connection := NewConn() connection.Command("HGETALL").ExpectMap(map[string]string{ "key1": "value1", }) connection.Command("HGETALL").ExpectMap(map[string]string{ "key2": "value2", }) if len(connection.commands) != 1 { t.Fatalf("Wrong number of registered commands. Expected '1' and got '%d'", len(connection.commands)) } cmd := connection.commands[0] if cmd.responses[0].response == nil { t.Fatal("Response not defined") } values, ok := cmd.responses[0].response.([]interface{}) if !ok { t.Fatal("Not storing response in the correct type") } expected := []string{"key2", "value2"} if len(values) != len(expected) { t.Fatal("Map values not stored properly") } for i := 0; i < len(expected); i++ { value, ok := values[i].([]byte) if ok { if string(value) != expected[i] { t.Errorf("Changing the response content. Expected '%s' and got '%s'", expected[i], string(value)) } } else { t.Error("Not storing the map content in byte format") } } } func TestExpectError(t *testing.T) { connection := NewConn() connection.Command("HGETALL").ExpectError(fmt.Errorf("error")) if len(connection.commands) != 1 { t.Fatalf("Did not registered the command. Expected '1' and got '%d'", len(connection.commands)) } cmd := connection.commands[0] if cmd.responses[0].err == nil { t.Fatal("Error not defined") } if cmd.responses[0].err.Error() != "error" { t.Fatal("Storing wrong error") } } func TestExpectPanic(t *testing.T) { connection := NewConn() connection.Command("HGETALL").ExpectPanic("panic") if len(connection.commands) != 1 { t.Fatalf("Did not registered the command. Expected '1' and got '%d'", len(connection.commands)) } cmd := connection.commands[0] if cmd.responses[0].panicVal == nil { t.Fatal("Panic not defined") } if cmd.responses[0].panicVal != "panic" { t.Fatal("Storing wrong panic message") } } func TestExpectSlice(t *testing.T) { connection := NewConn() field1 := []byte("hello") connection.Command("HMGET", "key", "field1", "field2").ExpectSlice(field1, nil) if len(connection.commands) != 1 { t.Fatalf("Did not registered the command. Expected '1' and got '%d'", len(connection.commands)) } reply, err := redis.ByteSlices(connection.Do("HMGET", "key", "field1", "field2")) if err != nil { t.Fatal(err) } if string(reply[0]) != string(field1) { t.Fatalf("reply[0] not hello but %s", string(reply[0])) } if reply[1] != nil { t.Fatal("reply[1] not nil") } } func TestExpectSliceFromStrings(t *testing.T) { connection := NewConn() field1 := "hello" field2 := "redigo" connection.Command("HMGET", "key", "field1", "field2").ExpectStringSlice(field1, field2) if len(connection.commands) != 1 { t.Fatalf("Did not registered the command. Expected '1' and got '%d'", len(connection.commands)) } reply, err := redis.Strings(connection.Do("HMGET", "key", "field1", "field2")) if err != nil { t.Fatal(err) } if reply[0] != field1 { t.Fatalf("reply[0] not %s but %s", field1, reply[0]) } if reply[1] != field2 { t.Fatalf("reply[1] not %s but %s", field2, reply[1]) } } func TestFind(t *testing.T) { connection := NewConn() connection.Command("HGETALL", "a", "b", "c") if connection.find("HGETALL", []interface{}{"a"}) != nil { t.Error("Returning command without comparing all registered arguments") } if connection.find("HGETALL", []interface{}{"a", "b", "c", "d"}) != nil { t.Error("Returning command without comparing all informed arguments") } if connection.find("HSETALL", []interface{}{"a", "b", "c"}) != nil { t.Error("Returning command when the name is different") } if connection.find("HGETALL", []interface{}{"a", "b", "c"}) == nil { t.Error("Could not find command with arguments in the same order") } } func TestRemoveRelatedCommands(t *testing.T) { connection := NewConn() connection.Command("HGETALL", "a", "b", "c") // 1 connection.Command("HGETALL", "a", "b", "c") // omit connection.Command("HGETALL", "c", "b", "a") // 2 connection.Command("HGETALL") // 3 connection.Command("HSETALL", "c", "b", "a") // 4 connection.Command("HSETALL") // 5 if len(connection.commands) != 5 { t.Errorf("Not removing related commands. Expected '5' and got '%d'", len(connection.commands)) } } func TestMatch(t *testing.T) { data := []struct { cmd *Cmd commandName string args []interface{} equal bool }{ { cmd: &Cmd{name: "HGETALL", args: []interface{}{"a", "b", "c"}}, commandName: "HGETALL", args: []interface{}{"a", "b", "c"}, equal: true, }, { cmd: &Cmd{name: "HGETALL", args: []interface{}{"a", []byte("abcdef"), "c"}}, commandName: "HGETALL", args: []interface{}{"a", []byte("abcdef"), "c"}, equal: true, }, { cmd: &Cmd{name: "HGETALL", args: []interface{}{"a", "b", "c"}}, commandName: "HGETALL", args: []interface{}{"c", "b", "a"}, equal: false, }, { cmd: &Cmd{name: "HGETALL", args: []interface{}{"a", "b", "c"}}, commandName: "HGETALL", args: []interface{}{"a", "b"}, equal: false, }, { cmd: &Cmd{name: "HGETALL", args: []interface{}{"a", "b"}}, commandName: "HGETALL", args: []interface{}{"a", "b", "c"}, equal: false, }, { cmd: &Cmd{name: "HGETALL", args: []interface{}{"a", "b", "c"}}, commandName: "HSETALL", args: []interface{}{"a", "b", "c"}, equal: false, }, { cmd: &Cmd{name: "HSETALL", args: nil}, commandName: "HSETALL", args: nil, equal: true, }, } for i, item := range data { e := match(item.commandName, item.args, item.cmd) if e != item.equal && item.equal { t.Errorf("Expected commands to be equal for data item '%d'", i) } else if e != item.equal && !item.equal { t.Errorf("Expected commands to be different for data item '%d'", i) } } } func TestHash(t *testing.T) { data := []struct { cmd *Cmd expected cmdHash }{ { cmd: &Cmd{name: "HGETALL", args: []interface{}{"a", "b", "c"}}, expected: cmdHash("HGETALLabc"), }, { cmd: &Cmd{name: "HGETALL", args: []interface{}{"a", []byte("abcdef"), "c"}}, expected: "HGETALLa[97 98 99 100 101 102]c", }, } for i, item := range data { if hash := item.cmd.hash(); hash != item.expected { t.Errorf("Expected “%s” and got “%s” for data item “%d”", item.expected, hash, i) } } } func TestRace(t *testing.T) { funcs := []func(*Cmd){ func(c *Cmd) { _ = equal("GET", []interface{}{[]byte("hello")}, c) }, func(c *Cmd) { _ = match("GET", []interface{}{[]byte("hello")}, c) }, func(c *Cmd) { _ = c.hash() }, func(c *Cmd) { _ = c.Called() }, func(c *Cmd) { _ = c.getResponse() }, func(c *Cmd) { c.Expect([]byte("OK")) }, func(c *Cmd) { c.ExpectMap(map[string]string{"hello": "world"}) }, func(c *Cmd) { c.ExpectError(fmt.Errorf("oh no")) }, func(c *Cmd) { c.ExpectPanic(fmt.Errorf("oh no")) }, func(c *Cmd) { c.ExpectSlice([]byte("hello"), []byte("world")) }, func(c *Cmd) { c.ExpectStringSlice("hello", "world") }, func(c *Cmd) { c.Handle(func(args []interface{}) (interface{}, error) { return nil, nil }) }, } // include two copies of each function, in case a function races with // itself funcs = append(funcs, funcs...) // run the test several times, with shuffled order, to make sure it's not // too order-dependent rand := rand.New(rand.NewSource(0)) for i := 0; i < 10; i++ { rand.Shuffle(len(funcs), func(i, j int) { funcs[i], funcs[j] = funcs[j], funcs[i] }) cmd := Cmd{name: "GET", args: []interface{}{[]byte("hello")}} var wg sync.WaitGroup wg.Add(len(funcs)) for _, f := range funcs { f := f go func() { f(&cmd) wg.Done() }() } wg.Wait() // we should have 12 to 14 responses, depending on how many of the // getResponse calls ran before at least two Expects, since getResponse // pops a response only if there are at least two responses l := len(cmd.responses) if l < 12 || l > 14 { t.Errorf("wanted 12-14 responses, got %v: %v", l, cmd.responses) } } } golang-github-rafaeljusto-redigomock-3.1.2/doc.go000066400000000000000000000150001451114530700217730ustar00rootroot00000000000000// Copyright 2014 Rafael Dantas Justo. All rights reserved. // Use of this source code is governed by a GPL // license that can be found in the LICENSE file. // Package redigomock is a mock for redigo library (redis client) // // Redigomock basically register the commands with the expected results in a internal global // variable. When the command is executed via Conn interface, the mock will look to this global // variable to retrieve the corresponding result. // // To start a mocked connection just do the following: // // c := redigomock.NewConn() // // Now you can inject it whenever your system needs a redigo.Conn because it satisfies all interface // requirements. Before running your tests you need beyond of mocking the connection, registering // the expected results. For that you can generate commands with the expected results. // // c.Command("HGETALL", "person:1").Expect("Person!") // c.Command( // "HMSET", []string{"person:1", "name", "John"}, // ).Expect("ok") // // As the Expect method from Command receives anything (interface{}), another method was created to // easy map the result to your structure. For that use ExpectMap: // // c.Command("HGETALL", "person:1").ExpectMap(map[string]string{ // "name": "John", // "age": 42, // }) // // You should also test the error cases, and you can do it in the same way of a normal result. // // c.Command("HGETALL", "person:1").ExpectError(fmt.Errorf("Low level error!")) // // Sometimes you will want to register a command regardless the arguments, and you can do it with // the method GenericCommand (mainly with the HMSET). // // c.GenericCommand("HMSET").Expect("ok") // // All commands are registered in a global variable, so they will be there until all your test cases // ends. So for good practice in test writing you should in the beginning of each test case clear // the mock states. // // c.Clear() // // Let's see a full test example. Imagine a Person structure and a function that pick up this // person in Redis using redigo library (file person.go): // // package person // // import ( // "fmt" // "github.com/gomodule/redigo/redis" // ) // // type Person struct { // Name string `redis:"name"` // Age int `redis:"age"` // } // // func RetrievePerson(conn redis.Conn, id string) (Person, error) { // var person Person // // values, err := redis.Values(conn.Do("HGETALL", fmt.Sprintf("person:%s", id))) // if err != nil { // return person, err // } // // err = redis.ScanStruct(values, &person) // return person, err // } // // Now we need to test it, so let's create the corresponding test with redigomock // (fileperson_test.go): // // package person // // import ( // "github.com/rafaeljusto/redigomock/v3" // "testing" // ) // // func TestRetrievePerson(t *testing.T) { // conn := redigomock.NewConn() // cmd := conn.Command("HGETALL", "person:1").ExpectMap(map[string]string{ // "name": "Mr. Johson", // "age": "42", // }) // // person, err := RetrievePerson(conn, "1") // if err != nil { // t.Fatal(err) // } // // if conn.Stats(cmd) != 1 { // t.Fatal("Command was not called!") // } // // if person.Name != "Mr. Johson" { // t.Errorf("Invalid name. Expected 'Mr. Johson' and got '%s'", person.Name) // } // // if person.Age != 42 { // t.Errorf("Invalid age. Expected '42' and got '%d'", person.Age) // } // } // // func TestRetrievePersonError(t *testing.T) { // conn := redigomock.NewConn() // conn.Command("HGETALL", "person:1").ExpectError(fmt.Errorf("Simulate error!")) // // person, err = RetrievePerson(conn, "1") // if err == nil { // t.Error("Should return an error!") // } // } // // When you use redis as a persistent list, then you might want to call the // same redis command multiple times. For example: // // func PollForData(conn redis.Conn) error { // var url string // var err error // // for { // if url, err = conn.Do("LPOP", "URLS"); err != nil { // return err // } // // go func(input string) { // // do something with the input // }(url) // } // // panic("Shouldn't be here") // } // // To test it, you can chain redis responses. Let's write a test case: // // func TestPollForData(t *testing.T) { // conn := redigomock.NewConn() // conn.Command("LPOP", "URLS"). // Expect("www.some.url.com"). // Expect("www.another.url.com"). // ExpectError(redis.ErrNil) // // if err := PollForData(conn); err != redis.ErrNil { // t.Error("This should return redis nil Error") // } // } // // In the first iteration of the loop redigomock would return // "www.some.url.com", then "www.another.url.com" and finally redis.ErrNil. // // Sometimes providing expected arguments to redigomock at compile time could // be too constraining. Let's imagine you use redis hash sets to store some // data, along with the timestamp of the last data update. Let's expand our // Person struct: // // type Person struct { // Name string `redis:"name"` // Age int `redis:"age"` // UpdatedAt uint64 `redis:updatedat` // Phone string `redis:phone` // } // // And add a function updating personal data (phone number for example). // Please notice that the update timestamp can't be determined at compile time: // // func UpdatePersonalData(conn redis.Conn, id string, person Person) error { // _, err := conn.Do("HMSET", fmt.Sprint("person:", id), "name", person.Name, "age", person.Age, "updatedat" , time.Now.Unix(), "phone" , person.Phone) // return err // } // // Unit test: // // func TestUpdatePersonalData(t *testing.T){ // redigomock.Clear() // // person := Person{ // Name : "A name", // Age : 18 // Phone : "123456" // } // // conn := redigomock.NewConn() // conn.Commmand("HMSET", "person:1", "name", person.Name, "age", person.Age, "updatedat", redigomock.NewAnyInt(), "phone", person.Phone).Expect("OK!") // // err := UpdatePersonalData(conn, "1", person) // if err != nil { // t.Error("This shouldn't return any errors") // } // } // // As you can see at the position of current timestamp redigomock is told to // match AnyInt struct created by NewAnyInt() method. AnyInt struct will match // any integer passed to redigomock from the tested method. Please see // fuzzyMatch.go file for more details. // // The interface of Conn which matches redigo.Conn is safe for concurrent use, // but the mock-only methods and fields, like Command and Errors, should not be // accessed concurrently with such calls. package redigomock golang-github-rafaeljusto-redigomock-3.1.2/fuzzy_match.go000066400000000000000000000033011451114530700235720ustar00rootroot00000000000000package redigomock import "reflect" // FuzzyMatcher is an interface that exports one function. It can be // passed to the Command as an argument. When the command is evaluated against // data provided in mock connection Do call, FuzzyMatcher will call Match on the // argument and return true if the argument fulfills constraints set in concrete // implementation type FuzzyMatcher interface { // Match takes an argument passed to mock connection Do call and checks if // it fulfills constraints set in concrete implementation of this interface Match(interface{}) bool } // NewAnyInt returns a FuzzyMatcher instance matching any integer passed as an // argument func NewAnyInt() FuzzyMatcher { return anyInt{} } // NewAnyDouble returns a FuzzyMatcher instance matching any double passed as // an argument func NewAnyDouble() FuzzyMatcher { return anyDouble{} } // NewAnyData returns a FuzzyMatcher instance matching every data type passed // as an argument (returns true by default) func NewAnyData() FuzzyMatcher { return anyData{} } type anyInt struct{} func (matcher anyInt) Match(input interface{}) bool { switch input.(type) { case int, int8, int16, int32, int64, uint8, uint16, uint32, uint64: return true default: return false } } type anyDouble struct{} func (matcher anyDouble) Match(input interface{}) bool { switch input.(type) { case float32, float64: return true default: return false } } type anyData struct{} func (matcher anyData) Match(input interface{}) bool { return true } func implementsFuzzy(input interface{}) bool { inputType := reflect.TypeOf(input) if inputType == nil { return false } return inputType.Implements(reflect.TypeOf((*FuzzyMatcher)(nil)).Elem()) } golang-github-rafaeljusto-redigomock-3.1.2/fuzzy_match_test.go000066400000000000000000000125021451114530700246340ustar00rootroot00000000000000package redigomock import "testing" func TestFuzzyCommandMatchAnyInt(t *testing.T) { fuzzyCommandTestInput := []struct { arguments []interface{} match bool }{ {[]interface{}{"TEST_COMMAND", "Test string", 1}, true}, {[]interface{}{"TEST_COMMAND", "Test string", 1234567}, true}, {[]interface{}{"TEST_COMMAND", 1, "Test string"}, false}, {[]interface{}{"TEST_COMMAND", "Test string", 1, 1}, false}, {[]interface{}{"TEST_COMMAND", "AnotherString", 1}, false}, {[]interface{}{"TEST_COMMAND", 1}, false}, {[]interface{}{"TEST_COMMAND", "AnotherString"}, false}, {[]interface{}{"TEST_COMMAND", "Test string", 1.0}, false}, {[]interface{}{"TEST_COMMAND", "Test string", "This is not an int"}, false}, {[]interface{}{"ANOTHER_COMMAND", "Test string", 1}, false}, } command := &Cmd{ name: "TEST_COMMAND", args: []interface{}{"Test string", NewAnyInt()}, } for pos, element := range fuzzyCommandTestInput { if retVal := match(element.arguments[0].(string), element.arguments[1:], command); retVal != element.match { t.Fatalf("comparing fuzzy comand failed. Comparison between comand [%#v] and test arguments : [%#v] at position %v returned %v while it should have returned %v", command, element.arguments, pos, retVal, element.match) } } } func TestFuzzyCommandMatchAnyDouble(t *testing.T) { fuzzyCommandTestInput := []struct { arguments []interface{} match bool }{ {[]interface{}{"TEST_COMMAND", "Test string", 1.123}, true}, {[]interface{}{"TEST_COMMAND", "Test string", 1234567.89}, true}, {[]interface{}{"TEST_COMMAND", 1.0, "Test string"}, false}, {[]interface{}{"TEST_COMMAND", "Test string", 1.123, 11.22}, false}, {[]interface{}{"TEST_COMMAND", "AnotherString", 1.1111}, false}, {[]interface{}{"TEST_COMMAND", 1.122}, false}, {[]interface{}{"TEST_COMMAND", "AnotherString"}, false}, {[]interface{}{"TEST_COMMAND", "Test string", 1}, false}, {[]interface{}{"TEST_COMMAND", "Test string", "This is not a double"}, false}, {[]interface{}{"ANOTHER_COMMAND", "Test string", 1.123}, false}, } command := &Cmd{ name: "TEST_COMMAND", args: []interface{}{"Test string", NewAnyDouble()}, } for pos, element := range fuzzyCommandTestInput { if retVal := match(element.arguments[0].(string), element.arguments[1:], command); retVal != element.match { t.Errorf("comparing fuzzy comand failed. Comparison between comand [%+v] and test arguments : [%v] at position %v returned %v while it should have returned %v", command, element.arguments, pos, retVal, element.match) } } } func TestFuzzyCommandMatchAnyData(t *testing.T) { fuzzyCommandTestInput := []struct { arguments []interface{} match bool }{ {[]interface{}{"TEST_COMMAND", "Test string", "Another string"}, true}, {[]interface{}{"TEST_COMMAND", "Test string", 12344}, true}, {[]interface{}{"TEST_COMMAND", "Test string", func() {}}, true}, // func {[]interface{}{"TEST_COMMAND", "Test string", []string{"Slice of", "strings"}}, true}, {[]interface{}{"TEST_COMMAND", "Test string", "Another string", 11.22}, false}, } command := &Cmd{ name: "TEST_COMMAND", args: []interface{}{"Test string", NewAnyData()}, } for pos, element := range fuzzyCommandTestInput { if retVal := match(element.arguments[0].(string), element.arguments[1:], command); retVal != element.match { t.Errorf("comparing fuzzy comand failed. Comparison between comand [%+v] and test arguments : [%v] at position %v returned %v while it should have returned %v", command, element.arguments, pos, retVal, element.match) } } } func TestFindWithFuzzy(t *testing.T) { connection := NewConn() connection.Command("HGETALL", NewAnyInt(), NewAnyDouble(), "Test string") if connection.find("HGETALL", []interface{}{1, 2.0}) != nil { t.Error("Returning command without comparing all registered arguments") } if connection.find("HGETALL", []interface{}{1, 2.0, "Test string", "a"}) != nil { t.Error("Returning command without comparing all informed arguments") } if connection.find("HSETALL", []interface{}{1, 2.0, "Test string"}) != nil { t.Error("Returning command when the name is different") } if connection.find("HGETALL", []interface{}{1.0, "Test string", 2}) != nil { t.Error("Returning command with arguments in a different order") } if connection.find("HGETALL", []interface{}{1, 2.0, "Test string"}) == nil { t.Error("Could not find command with arguments in the same order") } } func TestRemoveRelatedFuzzyCommands(t *testing.T) { connection := NewConn() connection.Command("HGETALL", 1, 2.0, "c") // saved , non fuzzy connection.Command("HGETALL", NewAnyInt(), 2.0, "c") // saved , fuzzy connection.Command("HGETALL", NewAnyInt(), 2.0, "c") // not saved!! , fuzzy connection.Command("HGETALL", NewAnyDouble(), 2.0, "c") // saved , fuzzy connection.Command("COMMAND2", NewAnyInt(), 2.0, "c") // saved , fuzzy connection.Command("HGETALL", NewAnyInt(), 5.0, "c") // saved, fuzzy connection.Command("HGETALL", NewAnyInt(), 2.0, "d") // saved, fuzzy connection.Command("HGETALL", NewAnyInt(), 2, "c") // saved, fuzzy connection.Command("HGETALL", NewAnyInt(), 2.0, "c", "d") // saved, fuzzy connection.Command("HGETALL", 1, NewAnyDouble(), "c") // saved, fuzzy if len(connection.commands) != 9 { t.Errorf("Non fuzzy command cound invalid, expected 9, got %d", len(connection.commands)) } } golang-github-rafaeljusto-redigomock-3.1.2/go.mod000066400000000000000000000001401451114530700220040ustar00rootroot00000000000000module github.com/rafaeljusto/redigomock/v3 go 1.15 require github.com/gomodule/redigo v1.8.8 golang-github-rafaeljusto-redigomock-3.1.2/go.sum000066400000000000000000000022531451114530700220400ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gomodule/redigo v1.8.8 h1:f6cXq6RRfiyrOJEV7p3JhLDlmawGBVBBP1MggY8Mo4E= github.com/gomodule/redigo v1.8.8/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= golang-github-rafaeljusto-redigomock-3.1.2/redigomock.go000066400000000000000000000246231451114530700233640ustar00rootroot00000000000000// Copyright 2014 Rafael Dantas Justo. All rights reserved. // Use of this source code is governed by a GPL // license that can be found in the LICENSE file. package redigomock import ( "context" "crypto/sha1" "encoding/hex" "fmt" "sync" "time" ) type queueElement struct { commandName string args []interface{} } type replyElement struct { reply interface{} err error } // Conn is the struct that can be used where you inject the redigo.Conn on // your project. // // The fields of Conn should not be modified after first use. (Sending to // ReceiveNow is safe.) type Conn struct { ReceiveWait bool // When set to true, Receive method will wait for a value in ReceiveNow channel to proceed, this is useful in a PubSub scenario ReceiveNow chan bool // Used to lock Receive method to simulate a PubSub scenario CloseMock func() error // Mock the redigo Close method ErrMock func() error // Mock the redigo Err method FlushMock func() error // Mock the redigo Flush method FlushSkippableMock func() error // Mock the redigo Flush method, will be ignore if return with a nil. commands []*Cmd // Slice that stores all registered commands for each connection queue []queueElement // Slice that stores all queued commands for each connection replies []replyElement // Slice that stores all queued replies subResponses []response // Queue responses for PubSub stats map[cmdHash]int // Command calls counter errors []error // Storage of all error occured in do functions mu sync.RWMutex // Hold while accessing any mutable fields } // NewConn returns a new mocked connection. Obviously as we are mocking we // don't need any Redis connection parameter func NewConn() *Conn { return &Conn{ ReceiveNow: make(chan bool), stats: make(map[cmdHash]int), } } // Close can be mocked using the Conn struct attributes func (c *Conn) Close() error { if c.CloseMock == nil { return nil } return c.CloseMock() } // Err can be mocked using the Conn struct attributes func (c *Conn) Err() error { if c.ErrMock == nil { return nil } return c.ErrMock() } // Command register a command in the mock system using the same arguments of // a Do or Send commands. It will return a registered command object where // you can set the response or error func (c *Conn) Command(commandName string, args ...interface{}) *Cmd { cmd := &Cmd{ name: commandName, args: args, } c.mu.Lock() defer c.mu.Unlock() c.removeRelatedCommands(commandName, args) c.commands = append(c.commands, cmd) return cmd } // Script registers a command in the mock system just like Command method // would do. The first argument is a byte array with the script text, next // ones are the ones you would pass to redis Script.Do() method func (c *Conn) Script(scriptData []byte, keyCount int, args ...interface{}) *Cmd { h := sha1.New() h.Write(scriptData) sha1sum := hex.EncodeToString(h.Sum(nil)) newArgs := make([]interface{}, 2+len(args)) newArgs[0] = sha1sum newArgs[1] = keyCount copy(newArgs[2:], args) return c.Command("EVALSHA", newArgs...) } // GenericCommand register a command without arguments. If a command with // arguments doesn't match with any registered command, it will look for // generic commands before throwing an error func (c *Conn) GenericCommand(commandName string) *Cmd { cmd := &Cmd{ name: commandName, } c.mu.Lock() defer c.mu.Unlock() c.removeRelatedCommands(commandName, nil) c.commands = append(c.commands, cmd) return cmd } // find will scan the registered commands, looking for the first command with // the same name and arguments. If the command is not found nil is returned // // Caller must hold c.mu. func (c *Conn) find(commandName string, args []interface{}) *Cmd { for _, cmd := range c.commands { if match(commandName, args, cmd) { return cmd } } return nil } // removeRelatedCommands verify if a command is already registered, removing // any command already registered with the same name and arguments. This // should avoid duplicated mocked commands. // // Caller must hold c.mu. func (c *Conn) removeRelatedCommands(commandName string, args []interface{}) { var unique []*Cmd for _, cmd := range c.commands { // new array will contain only commands that are not related to the given // one if !equal(commandName, args, cmd) { unique = append(unique, cmd) } } c.commands = unique } // Clear removes all registered commands. Useful for connection reuse in test // scenarios func (c *Conn) Clear() { c.mu.Lock() defer c.mu.Unlock() c.commands = []*Cmd{} c.queue = []queueElement{} c.replies = []replyElement{} c.stats = make(map[cmdHash]int) } // Do looks in the registered commands (via Command function) if someone // matches with the given command name and arguments, if so the corresponding // response or error is returned. If no registered command is found an error // is returned func (c *Conn) Do(commandName string, args ...interface{}) (reply interface{}, err error) { c.mu.Lock() defer c.mu.Unlock() if commandName == "" { if err := c.flush(); err != nil { return nil, err } if len(c.replies) == 0 { return nil, nil } replies := []interface{}{} for _, v := range c.replies { if v.err != nil { return nil, v.err } replies = append(replies, v.reply) } c.replies = []replyElement{} return replies, nil } if len(c.queue) != 0 || len(c.replies) != 0 { if err := c.flush(); err != nil { return nil, err } for _, v := range c.replies { if v.err != nil { return nil, v.err } } c.replies = []replyElement{} } return c.do(commandName, args...) } // Caller must hold c.mu. func (c *Conn) do(commandName string, args ...interface{}) (reply interface{}, err error) { cmd := c.find(commandName, args) if cmd == nil { // Didn't find a specific command, try to get a generic one if cmd = c.find(commandName, nil); cmd == nil { var msg string for _, regCmd := range c.commands { if commandName == regCmd.name { if len(msg) == 0 { msg = ". Possible matches are with the arguments:" } msg += fmt.Sprintf("\n* %#v", regCmd.args) } } err := fmt.Errorf("command %s with arguments %#v not registered in redigomock library%s", commandName, args, msg) c.errors = append(c.errors, err) return nil, err } } c.stats[cmd.hash()]++ response := cmd.getResponse() if response == nil { return nil, nil } if response.panicVal != nil { panic(response.panicVal) } if handler, ok := response.response.(ResponseHandler); ok { return handler(args) } return response.response, response.err } // DoWithTimeout is a helper function for Do call to satisfy the ConnWithTimeout // interface. func (c *Conn) DoWithTimeout(readTimeout time.Duration, cmd string, args ...interface{}) (interface{}, error) { return c.Do(cmd, args...) } // DoContext is a helper function for Do call to satisfy the ConnWithContext // interface. func (c *Conn) DoContext(ctx context.Context, cmd string, args ...interface{}) (reply interface{}, err error) { return c.Do(cmd, args...) } // Send stores the command and arguments to be executed later (by the Receive // function) in a first-come first-served order func (c *Conn) Send(commandName string, args ...interface{}) error { c.mu.Lock() defer c.mu.Unlock() c.queue = append(c.queue, queueElement{ commandName: commandName, args: args, }) return nil } // Flush can be mocked using the Conn struct attributes func (c *Conn) Flush() error { c.mu.Lock() defer c.mu.Unlock() return c.flush() } // Caller must hold c.mu. func (c *Conn) flush() error { if c.FlushMock != nil { return c.FlushMock() } if c.FlushSkippableMock != nil { if err := c.FlushSkippableMock(); err != nil { return err } } if len(c.queue) > 0 { for _, cmd := range c.queue { reply, err := c.do(cmd.commandName, cmd.args...) c.replies = append(c.replies, replyElement{reply: reply, err: err}) } c.queue = []queueElement{} } return nil } // AddSubscriptionMessage register a response to be returned by the receive // call. func (c *Conn) AddSubscriptionMessage(msg interface{}) { resp := response{} resp.response = msg c.mu.Lock() defer c.mu.Unlock() c.subResponses = append(c.subResponses, resp) } // Receive will process the queue created by the Send method, only one item // of the queue is processed by Receive call. It will work as the Do method func (c *Conn) Receive() (reply interface{}, err error) { if c.ReceiveWait { <-c.ReceiveNow } c.mu.Lock() defer c.mu.Unlock() if len(c.queue) == 0 && len(c.replies) == 0 { if len(c.subResponses) > 0 { reply, err = c.subResponses[0].response, c.subResponses[0].err c.subResponses = c.subResponses[1:] return } return nil, fmt.Errorf("no more items") } if err := c.flush(); err != nil { return nil, err } reply, err = c.replies[0].reply, c.replies[0].err c.replies = c.replies[1:] return } // ReceiveWithTimeout is a helper function for Receive call to satisfy the // ConnWithTimeout interface. func (c *Conn) ReceiveWithTimeout(timeout time.Duration) (interface{}, error) { return c.Receive() } // ReceiveContext is a helper function for Receive call to satisfy the // ConnWithContext interface. func (c *Conn) ReceiveContext(ctx context.Context) (reply interface{}, err error) { return c.Receive() } // Stats returns the number of times that a command was called in the current // connection func (c *Conn) Stats(cmd *Cmd) int { c.mu.Lock() defer c.mu.Unlock() return c.stats[cmd.hash()] } // ExpectationsWereMet can guarantee that all commands that was set on unit tests // called or call of unregistered command can be caught here too func (c *Conn) ExpectationsWereMet() error { c.mu.Lock() defer c.mu.Unlock() errMsg := "" for _, err := range c.errors { errMsg = fmt.Sprintf("%s%s\n", errMsg, err.Error()) } for _, cmd := range c.commands { if !cmd.Called() { errMsg = fmt.Sprintf("%sCommand %s with arguments %#v expected but never called.\n", errMsg, cmd.name, cmd.args) } } if errMsg != "" { return fmt.Errorf("%s", errMsg) } return nil } // Errors returns any errors that this connection returned in lieu of a valid // mock. func (c *Conn) Errors() []error { c.mu.Lock() defer c.mu.Unlock() // Return a copy of c.errors, in case caller wants to mutate it ret := make([]error, len(c.errors)) copy(ret, c.errors) return ret } golang-github-rafaeljusto-redigomock-3.1.2/redigomock_test.go000066400000000000000000000472021451114530700244210ustar00rootroot00000000000000package redigomock import ( "fmt" "reflect" "sync" "testing" "time" "github.com/gomodule/redigo/redis" ) var ( _ redis.Conn = &Conn{} _ redis.ConnWithTimeout = &Conn{} _ redis.ConnWithContext = &Conn{} ) type Person struct { Name string `redis:"name"` Age int `redis:"age"` } func RetrievePerson(conn redis.Conn, id string) (Person, error) { var person Person values, err := redis.Values(conn.Do("HGETALL", fmt.Sprintf("person:%s", id))) if err != nil { return person, err } err = redis.ScanStruct(values, &person) return person, err } func RetrievePeople(conn redis.Conn, ids []string) ([]Person, error) { var people []Person for _, id := range ids { conn.Send("HGETALL", fmt.Sprintf("person:%s", id)) } for i := 0; i < len(ids); i++ { values, err := redis.Values(conn.Receive()) if err != nil { return nil, err } var person Person err = redis.ScanStruct(values, &person) if err != nil { return nil, err } people = append(people, person) } return people, nil } func TestDoCommand(t *testing.T) { connection := NewConn() connection.Command("HGETALL", "person:1").ExpectMap(map[string]string{ "name": "Mr. Johson", "age": "42", }) person, err := RetrievePerson(connection, "1") if err != nil { t.Fatal(err) } if person.Name != "Mr. Johson" { t.Errorf("Invalid name. Expected 'Mr. Johson' and got '%s'", person.Name) } if person.Age != 42 { t.Errorf("Invalid age. Expected '42' and got '%d'", person.Age) } } func TestPanickyDoCommand(t *testing.T) { panicMsg := "panic-message" defer func() { r := recover() if r == nil { t.Errorf("Expected a panic to happen") } recoverMsg, ok := r.(string) if ok == false { t.Errorf("Expected the recovered panic value to be a string") } if recoverMsg != panicMsg { t.Errorf("Expected the recovered panic value to be %s", panicMsg) } }() connection := NewConn() connection.Command("HGETALL", "person:1").ExpectPanic(panicMsg) RetrievePerson(connection, "1") } func TestDoCommandMultipleReturnValues(t *testing.T) { connection := NewConn() connection.Command("HGETALL", "person:1").ExpectMap(map[string]string{ "name": "Mr. Johson", "age": "42", }).ExpectMap(map[string]string{ "name": "Ms. Jennifer", "age": "28", }).ExpectError(fmt.Errorf("simulated error")) person, err := RetrievePerson(connection, "1") if err != nil { t.Fatal(err) } if person.Name != "Mr. Johson" { t.Errorf("Invalid name. Expected 'Mr. Johson' and got '%s'", person.Name) } if person.Age != 42 { t.Errorf("Invalid age. Expected '42' and got '%d'", person.Age) } person, err = RetrievePerson(connection, "1") if err != nil { t.Fatal(err) } if person.Name != "Ms. Jennifer" { t.Errorf("Invalid name. Expected 'Mr. Johson' and got '%s'", person.Name) } if person.Age != 28 { t.Errorf("Invalid age. Expected '28' and got '%d'", person.Age) } _, err = RetrievePerson(connection, "1") if err == nil { t.Error("Should return an error!") } } func TestDoGenericCommand(t *testing.T) { connection := NewConn() connection.GenericCommand("HGETALL").ExpectMap(map[string]string{ "name": "Mr. Johson", "age": "42", }) person, err := RetrievePerson(connection, "1") if err != nil { t.Fatal(err) } if person.Name != "Mr. Johson" { t.Errorf("Invalid name. Expected 'Mr. Johson' and got '%s'", person.Name) } if person.Age != 42 { t.Errorf("Invalid age. Expected '42' and got '%d'", person.Age) } } func TestDoCommandWithGeneric(t *testing.T) { connection := NewConn() connection.Command("HGETALL", "person:1").ExpectMap(map[string]string{ "name": "Mr. Johson", "age": "42", }) connection.GenericCommand("HGETALL").ExpectMap(map[string]string{ "name": "Mr. Mark", "age": "32", }) person, err := RetrievePerson(connection, "1") if err != nil { t.Fatal(err) } if person.Name != "Mr. Johson" { t.Errorf("Invalid name. Expected 'Mr. Johson' and got '%s'", person.Name) } if person.Age != 42 { t.Errorf("Invalid age. Expected '42' and got '%d'", person.Age) } } func TestDoCommandWithError(t *testing.T) { connection := NewConn() connection.Command("HGETALL", "person:1").ExpectError(fmt.Errorf("simulated error")) _, err := RetrievePerson(connection, "1") if err == nil { t.Error("Should return an error!") return } } func TestDoCommandWithUnexpectedCommand(t *testing.T) { connection := NewConn() _, err := RetrievePerson(connection, "X") if err == nil { t.Error("Should detect a command not registered!") return } } func TestDoCommandWithUnexpectedCommandWithSuggestions(t *testing.T) { connection := NewConn() connection.Command("HGETALL", "person:1").ExpectError(fmt.Errorf("simulated error")) _, err := RetrievePerson(connection, "X") if err == nil { t.Fatal("Should detect a command not registered!") } msg := `command HGETALL with arguments []interface {}{"person:X"} not registered in redigomock library. Possible matches are with the arguments: * []interface {}{"person:1"}` if err.Error() != msg { t.Errorf("Unexpected error message: %s", err.Error()) } } func TestDoCommandWithoutResponse(t *testing.T) { connection := NewConn() connection.Command("HGETALL", "person:1") _, err := RetrievePerson(connection, "1") if err == nil { t.Fatal("Returning an information when it shoudn't") } } func TestDoCommandWithNilArgument(t *testing.T) { connection := NewConn() connection.Command("MGET", "1", nil, "2").Expect([]interface{}{"result1", nil, "result2"}) rawValues, err := connection.Do("MGET", "1", nil, "2") if err != nil { t.Fatal(err) } values, ok := rawValues.([]interface{}) if !ok { t.Fatalf("unexpected returned type %T", rawValues) } if len(values) != 3 { t.Fatalf("unexpected number of values (%d)", len(values)) } if values[0] != "result1" { t.Errorf("unexpected value[0]: %v", values[0]) } if values[1] != nil { t.Errorf("unexpected value[1]: %v", values[1]) } if values[2] != "result2" { t.Errorf("unexpected value[2]: %v", values[2]) } } func TestDoEmptyCommand(t *testing.T) { c := NewConn() reply, err := c.Do("") if reply != nil || err != nil { t.Errorf("Expected nil values received: %v, %v", reply, err) } } func TestDoEmptyFlushPipeline(t *testing.T) { c := NewConn() cmds := []struct { cmd []string val string }{ { cmd: []string{"HGET", "somekey"}, val: "someval", }, { cmd: []string{"HGET", "anotherkey"}, val: "anotherval", }, } for _, cmd := range cmds { args := []interface{}{} for _, arg := range cmd.cmd[1:] { args = append(args, arg) } c.Command(cmd.cmd[0], args...).Expect(cmd.val) c.Send(cmd.cmd[0], args...) } reply, err := c.Do("") if err != nil { t.Errorf("Error received when trying to flush pipeline") } replies, ok := reply.([]interface{}) if !ok { t.Errorf("Didn't receive slice of replies received type '%T'", reply) } if len(replies) != len(cmds) { t.Errorf("Expected %d replies and received %d", len(cmds), len(replies)) } for i, r := range replies { val, ok := r.(string) if !ok { t.Errorf("Didn't receive string") } if cmds[i].val != val { t.Errorf("Didn't receive expected: %s != %s", cmds[i].val, val) } } } func TestSendFlushReceive(t *testing.T) { connection := NewConn() connection.Command("HGETALL", "person:1").ExpectMap(map[string]string{ "name": "Mr. Johson", "age": "42", }) connection.Command("HGETALL", "person:2").ExpectMap(map[string]string{ "name": "Ms. Jennifer", "age": "28", }) people, err := RetrievePeople(connection, []string{"1", "2"}) if err != nil { t.Fatal(err) } if len(people) != 2 { t.Errorf("Wrong number of people. Expected '2' and got '%d'", len(people)) } if people[0].Name != "Mr. Johson" || people[1].Name != "Ms. Jennifer" { t.Error("People name order are wrong") } if people[0].Age != 42 || people[1].Age != 28 { t.Error("People age order are wrong") } if _, err := connection.Receive(); err == nil { t.Error("Not detecting when there's no more items to receive") } } func TestSendReceiveWithWait(t *testing.T) { conn := NewConn() conn.ReceiveWait = true conn.Command("HGETALL", "person:1").ExpectMap(map[string]string{ "name": "Mr. Johson", "age": "42", }) conn.Command("HGETALL", "person:2").ExpectMap(map[string]string{ "name": "Ms. Jennifer", "age": "28", }) ids := []string{"1", "2"} for _, id := range ids { conn.Send("HGETALL", fmt.Sprintf("person:%s", id)) } var people []Person var peopleLock sync.RWMutex go func() { for i := 0; i < len(ids); i++ { values, err := redis.Values(conn.Receive()) if err != nil { t.Error(err) return } var person Person err = redis.ScanStruct(values, &person) if err != nil { t.Error(err) return } peopleLock.Lock() people = append(people, person) peopleLock.Unlock() } }() for i := 0; i < len(ids); i++ { conn.ReceiveNow <- true } time.Sleep(10 * time.Millisecond) peopleLock.RLock() defer peopleLock.RUnlock() if len(people) != 2 { t.Fatalf("Wrong number of people. Expected '2' and got '%d'", len(people)) } if people[0].Name != "Mr. Johson" || people[1].Name != "Ms. Jennifer" { t.Error("People name order are wrong") } if people[0].Age != 42 || people[1].Age != 28 { t.Error("People age order are wrong") } } func TestPubSub(t *testing.T) { channel := "subchannel" conn := NewConn() conn.ReceiveWait = true conn.Command("SUBSCRIBE", channel).Expect([]interface{}{ []byte("subscribe"), []byte(channel), []byte("1"), }) // check some values are correct if len(conn.commands) != 1 { t.Errorf("unexpected number of commands, expected '%d', got '%d'", 1, len(conn.commands)) } psc := redis.PubSubConn{Conn: conn} if err := psc.Subscribe(channel); err != nil { t.Error(err) } defer psc.Unsubscribe(channel) if err := conn.ExpectationsWereMet(); err != nil { t.Error(err) } messages := [][]byte{ []byte("value1"), []byte("value2"), []byte("value3"), []byte("finished"), } for _, message := range messages { conn.AddSubscriptionMessage([]interface{}{ []byte("message"), []byte(channel), message, }) } if len(conn.subResponses) != 4 { t.Errorf("unexpected number of sub-responses, expected '%d', got '%d'", 4, len(conn.subResponses)) } // function to trigger a new pub/sub message received from the redis server nextMessage := func() { conn.ReceiveNow <- true } // Should receive the subscription message first go nextMessage() switch msg := psc.Receive().(type) { case redis.Subscription: break default: t.Errorf("Expected subscribe message type but received '%T'", msg) } for _, expectedMessage := range messages { go nextMessage() switch msg := psc.Receive().(type) { case redis.Message: if !reflect.DeepEqual(msg.Data, expectedMessage) { t.Errorf("expected message '%s'and got '%s'", string(expectedMessage), string(msg.Data)) } default: t.Errorf("got wrong message type '%T' for message", msg) } } } func TestSendFlushReceiveWithError(t *testing.T) { connection := NewConn() connection.Command("HGETALL", "person:1").ExpectMap(map[string]string{ "name": "Mr. Johson", "age": "42", }) connection.Command("HGETALL", "person:2").ExpectMap(map[string]string{ "name": "Ms. Jennifer", "age": "28", }) connection.Command("HGETALL", "person:2").ExpectError(fmt.Errorf("simulated error")) _, err := RetrievePeople(connection, []string{"1", "2", "3"}) if err == nil { t.Error("Not detecting error when using send/flush/receive") } } func TestDummyFunctions(t *testing.T) { var conn Conn if conn.Close() != nil { t.Error("Close is not dummy!") } conn.CloseMock = func() error { return fmt.Errorf("close error") } if err := conn.Close(); err == nil || err.Error() != "close error" { t.Errorf("Not mocking Close method correctly. Expected “close error” and got “%v”", err) } if conn.Err() != nil { t.Error("Err is not dummy!") } conn.ErrMock = func() error { return fmt.Errorf("err error") } if err := conn.Err(); err == nil || err.Error() != "err error" { t.Errorf("Not mocking Err method correctly. Expected “err error” and got “%v”", err) } if conn.Flush() != nil { t.Error("Flush is not dummy!") } conn.FlushMock = func() error { return fmt.Errorf("flush error") } if err := conn.Flush(); err == nil || err.Error() != "flush error" { t.Errorf("Not mocking Flush method correctly. Expected “flush error” and got “%v”", err) } } func TestSkipDummyFlushFunction(t *testing.T) { connection := NewConn() errResult := false connection.Command("PING").Expect("PONG") connection.FlushSkippableMock = func() error { if errResult { return fmt.Errorf("flush error") } return nil } connection.Send("PING") err := connection.Flush() if err != nil { t.Errorf("Not mocking Flush method correctly. Got unexpected error “%v”", err) } s, err := connection.Receive() if err != nil || s != "PONG" { t.Errorf("Not mocking Flush method correctly. Got unexpected result “%v” and error “%v”", s, err) } errResult = true connection.Clear() connection.Command("PING").Expect("PONG") connection.Send("PING") err = connection.Flush() if err == nil || err.Error() != "flush error" { t.Errorf("Not mocking Flush method correctly. Expected “flush error” and got “%v”", err) } } func TestClear(t *testing.T) { connection := NewConn() connection.Command("HGETALL", "person:1").ExpectMap(map[string]string{ "name": "Mr. Johson", "age": "42", }) connection.Command("HGETALL", "person:2").ExpectMap(map[string]string{ "name": "Ms. Jennifer", "age": "28", }) connection.GenericCommand("HGETALL").ExpectMap(map[string]string{ "name": "Ms. Mark", "age": "32", }) connection.Do("HGETALL", "person:1") connection.Do("HGETALL", "person:2") connection.Clear() if len(connection.commands) > 0 { t.Error("Clear function not clearing registered commands") } if len(connection.queue) > 0 { t.Error("Clear function not clearing the queue") } if len(connection.stats) > 0 { t.Error("Clear function not clearing stats") } } func TestStats(t *testing.T) { connection := NewConn() cmd1 := connection.Command("HGETALL", "person:1").ExpectMap(map[string]string{ "name": "Mr. Johson", "age": "42", }).ExpectMap(map[string]string{ "name": "Mr. Johson", "age": "42", }) cmd2 := connection.Command("HGETALL", "person:2").ExpectMap(map[string]string{ "name": "Mr. Larry", "age": "27", }) if _, err := RetrievePerson(connection, "1"); err != nil { t.Fatal(err) } if _, err := RetrievePerson(connection, "1"); err != nil { t.Fatal(err) } if counter := connection.Stats(cmd1); counter != 2 { t.Errorf("Expected command cmd1 to be called 2 times, but it was called %d times", counter) } if counter := connection.Stats(cmd2); counter != 0 { t.Errorf("Expected command cmd2 to don't be called, but it was called %d times", counter) } } func TestDoFlushesQueue(t *testing.T) { connection := NewConn() cmd1 := connection.Command("MULTI") cmd2 := connection.Command("SET", "person-123", 123456) cmd3 := connection.Command("EXPIRE", "person-123", 1000) cmd4 := connection.Command("EXEC").Expect([]interface{}{"OK", "OK"}) connection.Send("MULTI") connection.Send("SET", "person-123", 123456) connection.Send("EXPIRE", "person-123", 1000) if _, err := connection.Do("EXEC"); err != nil { t.Fatal(err) } if counter := connection.Stats(cmd1); counter != 1 { t.Errorf("Expected cmd1 to be called once but was called %d times", counter) } if counter := connection.Stats(cmd2); counter != 1 { t.Errorf("Expected cmd2 to be called once but was called %d times", counter) } if counter := connection.Stats(cmd3); counter != 1 { t.Errorf("Expected cmd3 to be called once but was called %d times", counter) } if counter := connection.Stats(cmd4); counter != 1 { t.Errorf("Expected cmd4 to be called once but was called %d times", counter) } } func TestReceiveFallsBackOnGenericCommands(t *testing.T) { connection := NewConn() cmd1 := connection.Command("MULTI") cmd2 := connection.GenericCommand("SET") cmd3 := connection.GenericCommand("EXPIRE") cmd4 := connection.Command("EXEC") connection.Send("MULTI") connection.Send("SET", "person-123", 123456) connection.Send("EXPIRE", "person-123", 1000) connection.Send("EXEC") connection.Flush() connection.Receive() connection.Receive() connection.Receive() connection.Receive() if counter := connection.Stats(cmd1); counter != 1 { t.Errorf("Expected cmd1 to be called once but was called %d times", counter) } if counter := connection.Stats(cmd2); counter != 1 { t.Errorf("Expected cmd2 to be called once but was called %d times", counter) } if counter := connection.Stats(cmd3); counter != 1 { t.Errorf("Expected cmd3 to be called once but was called %d times", counter) } if counter := connection.Stats(cmd4); counter != 1 { t.Errorf("Expected cmd4 to be called once but was called %d times", counter) } } func TestReceiveReturnsErrorWithNoRegisteredCommand(t *testing.T) { connection := NewConn() connection.Command("SET", "person-123", "Councilman Jamm") connection.Send("GET", "person-123") connection.Flush() resp, err := connection.Receive() if err == nil { t.Errorf("Should have received an error when calling Receive with a command in the queue that was not registered") } if resp != nil { t.Errorf("Should have returned a nil response when calling Receive with a command in the queue that was not registered") } } func TestFailCommandOnTransaction(t *testing.T) { connection := NewConn() connection.Command("EXEC").Expect([]interface{}{"OK", "OK"}) connection.Send("MULTI") if _, err := connection.Do("EXEC"); err == nil { t.Errorf("Should have received an error when calling EXEC with a transaction with a command that was not registered") } } func TestAllCommandsCalled(t *testing.T) { connection := NewConn() connection.Command("GET", "hello").Expect("world") connection.Command("SET", "hello", "world") connection.Do("GET", "hello") // this error is expected err := connection.ExpectationsWereMet() if err == nil { t.Fatal("Should have received and error because SET command not called yet") } connection.Do("SET", "hello", "world") err = connection.ExpectationsWereMet() if err != nil { t.Fatal("Should have no error due to SET already called") } connection.Do("DEL", "hello") err = connection.ExpectationsWereMet() if err == nil { t.Fatal("Should have error due to DEL is unexpected") } } func TestDoRace(t *testing.T) { connection := NewConn() n := 100 connection.Command("GET", "hello").Expect("world") var wg sync.WaitGroup wg.Add(n) for i := 0; i < n; i++ { go func() { connection.Do("GET", "hello") wg.Done() }() } wg.Wait() err := connection.ExpectationsWereMet() if err != nil { t.Error("Called was not correctly set") } } func TestDoCommandWithHandler(t *testing.T) { connection := NewConn() connection.GenericCommand("PUBLISH").Handle(ResponseHandler(func(args []interface{}) (interface{}, error) { if len(args) != 2 { return nil, fmt.Errorf("unexpected number of arguments: %d", len(args)) } v, ok := args[1].(string) if !ok { return nil, fmt.Errorf("unexpected type %T", args[1]) } if v != "stuff" { return nil, fmt.Errorf("unexpected value '%s'", v) } return int64(1), nil })) clients, err := redis.Int64(connection.Do("PUBLISH", "ch", "stuff")) if err != nil { t.Fatal(err) } if clients != 1 { t.Errorf("unexpected number of notified clients '%d'", clients) } } func TestErrorRace(t *testing.T) { connection := NewConn() n := 100 var wg sync.WaitGroup wg.Add(n) for i := 0; i < n; i++ { go func() { connection.Do("GET", "hello") wg.Done() }() } wg.Wait() if len(connection.Errors()) != n { t.Errorf("wanted %v errors, got %v", n, len(connection.Errors())) } }