pax_global_header00006660000000000000000000000064137666464770014544gustar00rootroot0000000000000052 comment=5b2c339561a88d8ec6e233c4053327188c790c00 redigomock-3.0.1/000077500000000000000000000000001376664647700136705ustar00rootroot00000000000000redigomock-3.0.1/.gitignore000066400000000000000000000004031376664647700156550ustar00rootroot00000000000000# 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 redigomock-3.0.1/.travis.yml000066400000000000000000000003341376664647700160010ustar00rootroot00000000000000language: 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 redigomock-3.0.1/AUTHORS000066400000000000000000000000441376664647700147360ustar00rootroot00000000000000Rafael Dantas Justo - @rafaeljusto redigomock-3.0.1/CONTRIBUTORS000066400000000000000000000006121376664647700155470ustar00rootroot00000000000000Ahmad 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 Štěpán Pilař - @mingan Zachery Moneypenny - @whazzmasterredigomock-3.0.1/Changelog000066400000000000000000000034421376664647700155050ustar00rootroot00000000000000# Change Log All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). ## [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 redigomock-3.0.1/LICENSE000066400000000000000000000431511376664647700147010ustar00rootroot00000000000000GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. {description} Copyright (C) {year} {fullname} This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. {signature of Ty Coon}, 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License.redigomock-3.0.1/README.md000066400000000000000000000110701376664647700151460ustar00rootroot00000000000000redigomock ========== [![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!") } ```redigomock-3.0.1/command.go000066400000000000000000000120361376664647700156370ustar00rootroot00000000000000// 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]) == false { 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]) == false { return false } } else if reflect.DeepEqual(cmd.args[pos], args[pos]) == false { 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{}{} for _, r := range resp { ifaces = append(ifaces, r) } 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 } redigomock-3.0.1/command_test.go000066400000000000000000000351351376664647700167030ustar00rootroot00000000000000// 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) } } } redigomock-3.0.1/doc.go000066400000000000000000000150001376664647700147600ustar00rootroot00000000000000// 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 redigomock-3.0.1/fuzzy_match.go000066400000000000000000000033011376664647700165570ustar00rootroot00000000000000package 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()) } redigomock-3.0.1/fuzzy_match_test.go000066400000000000000000000125021376664647700176210ustar00rootroot00000000000000package 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)) } } redigomock-3.0.1/go.mod000066400000000000000000000001401376664647700147710ustar00rootroot00000000000000module github.com/rafaeljusto/redigomock/v3 go 1.15 require github.com/gomodule/redigo v1.8.3 redigomock-3.0.1/go.sum000066400000000000000000000021631376664647700150250ustar00rootroot00000000000000github.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.3 h1:HR0kYDX2RJZvAup8CsiJwxB4dTCSC0AaUq6S4SiLwUc= github.com/gomodule/redigo v1.8.3/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= 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.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 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.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= redigomock-3.0.1/redigomock.go000066400000000000000000000237171376664647700163540ustar00rootroot00000000000000// 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" "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...) } // 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() } // 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 } redigomock-3.0.1/redigomock_test.go000066400000000000000000000471221376664647700174070ustar00rootroot00000000000000package redigomock import ( "fmt" "reflect" "sync" "testing" "time" "github.com/gomodule/redigo/redis" ) var ( _ redis.Conn = &Conn{} _ redis.ConnWithTimeout = &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.Fatal(err) } var person Person err = redis.ScanStruct(values, &person) if err != nil { t.Fatal(err) } 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) break } } } 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())) } }