├── .travis.yml ├── license ├── mgotest.go ├── mgotest_test.go ├── mgotestrs └── mgotestrs.go ├── patents ├── readme.md └── rs.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.3 5 | 6 | matrix: 7 | fast_finish: true 8 | 9 | before_install: 10 | - go get -v github.com/golang/lint/golint 11 | - go get -v code.google.com/p/go.tools/cmd/cover 12 | 13 | install: 14 | - go install -race -v std 15 | - go get -race -t -v ./... 16 | - go install -race -v ./... 17 | 18 | script: 19 | - $HOME/gopath/bin/golint . 20 | - go test -cpu=2 -race -v ./... 21 | - go test -cpu=2 -covermode=atomic ./... 22 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | For mgotest software 4 | 5 | Copyright (c) 2015, Facebook, Inc. All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name Facebook nor the names of its contributors may be used to 18 | endorse or promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /mgotest.go: -------------------------------------------------------------------------------- 1 | // Package mgotest provides standalone test instances of mongo sutable for use 2 | // in tests. 3 | package mgotest 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "os/exec" 11 | "text/template" 12 | "time" 13 | 14 | "gopkg.in/mgo.v2" 15 | 16 | "github.com/facebookgo/freeport" 17 | "github.com/facebookgo/testname" 18 | "github.com/facebookgo/waitout" 19 | ) 20 | 21 | var mgoWaitingForConnections = []byte("waiting for connections on port") 22 | 23 | var configTemplate, configTemplateErr = template.New("config").Parse(` 24 | bind_ip = 127.0.0.1 25 | dbpath = {{.DBPath}} 26 | nohttpinterface = true 27 | nojournal = true 28 | noprealloc = true 29 | nounixsocket = true 30 | nssize = 8 31 | port = {{.Port}} 32 | quiet = true 33 | smallfiles = true 34 | {{if .ReplSet}} 35 | oplogSize = 1 36 | replSet = rs 37 | {{end}} 38 | `) 39 | 40 | func init() { 41 | if configTemplateErr != nil { 42 | panic(configTemplateErr) 43 | } 44 | } 45 | 46 | // Fatalf is satisfied by testing.T or testing.B. 47 | type Fatalf interface { 48 | Fatalf(format string, args ...interface{}) 49 | } 50 | 51 | // Server is a unique instance of a mongod. 52 | type Server struct { 53 | Port int 54 | DBPath string 55 | ReplSet bool 56 | StopTimeout time.Duration 57 | T Fatalf 58 | cmd *exec.Cmd 59 | testName string 60 | } 61 | 62 | // Start the server, this will return once the server has been started. 63 | func (s *Server) Start(args ...string) { 64 | if s.Port == 0 { 65 | port, err := freeport.Get() 66 | if err != nil { 67 | s.T.Fatalf(err.Error()) 68 | } 69 | s.Port = port 70 | } 71 | if s.testName == "" { 72 | s.T.Fatalf("Cannot determine name for test") 73 | } 74 | dir, err := ioutil.TempDir("", "mgotest-dbpath-"+s.testName) 75 | if err != nil { 76 | s.T.Fatalf(err.Error()) 77 | } 78 | s.DBPath = dir 79 | 80 | cf, err := ioutil.TempFile(s.DBPath, "config-") 81 | if err != nil { 82 | s.T.Fatalf(err.Error()) 83 | } 84 | if err := configTemplate.Execute(cf, s); err != nil { 85 | s.T.Fatalf(err.Error()) 86 | } 87 | if err := cf.Close(); err != nil { 88 | s.T.Fatalf(err.Error()) 89 | } 90 | 91 | waiter := waitout.New(mgoWaitingForConnections) 92 | args = append(args, "--config", cf.Name(), "--setParameter", "enableTestCommands=1") 93 | s.cmd = exec.Command("mongod", args...) 94 | s.cmd.Env = envPlusLcAll() 95 | if os.Getenv("MGOTEST_VERBOSE") == "1" { 96 | s.cmd.Stdout = io.MultiWriter(os.Stdout, waiter) 97 | s.cmd.Stderr = os.Stderr 98 | } else { 99 | s.cmd.Stdout = waiter 100 | } 101 | if err := s.cmd.Start(); err != nil { 102 | s.T.Fatalf(err.Error()) 103 | } 104 | waiter.Wait() 105 | } 106 | 107 | // Stop the server, this will also remove all data. 108 | func (s *Server) Stop() { 109 | fin := make(chan struct{}) 110 | go func() { 111 | defer close(fin) 112 | s.cmd.Process.Kill() 113 | s.cmd.Process.Wait() 114 | os.RemoveAll(s.DBPath) 115 | }() 116 | select { 117 | case <-fin: 118 | case <-time.After(s.StopTimeout): 119 | } 120 | } 121 | 122 | // URL for the mongo server, suitable for use with mgo.Dial. 123 | func (s *Server) URL() string { 124 | return fmt.Sprintf("127.0.0.1:%d", s.Port) 125 | } 126 | 127 | // Session for the mongo server. 128 | func (s *Server) Session() *mgo.Session { 129 | session, err := mgo.Dial(s.URL()) 130 | if err != nil { 131 | s.T.Fatalf(err.Error()) 132 | } 133 | return session 134 | } 135 | 136 | // NewStartedServer creates a new server starts it. 137 | func NewStartedServer(t Fatalf, args ...string) *Server { 138 | for { 139 | s := &Server{ 140 | T: t, 141 | StopTimeout: 15 * time.Second, 142 | testName: testname.Get("MGO"), 143 | } 144 | start := make(chan struct{}) 145 | go func() { 146 | defer close(start) 147 | s.Start(args...) 148 | }() 149 | select { 150 | case <-start: 151 | return s 152 | case <-time.After(30 * time.Second): 153 | } 154 | } 155 | } 156 | 157 | // NewReplSetServer creates a new server starts it with ReplSet enabled. 158 | func NewReplSetServer(t Fatalf, args ...string) *Server { 159 | for { 160 | s := &Server{ 161 | T: t, 162 | StopTimeout: 15 * time.Second, 163 | ReplSet: true, 164 | testName: testname.Get("MGO"), 165 | } 166 | start := make(chan struct{}) 167 | go func() { 168 | defer close(start) 169 | s.Start(args...) 170 | }() 171 | select { 172 | case <-start: 173 | return s 174 | case <-time.After(30 * time.Second): 175 | } 176 | } 177 | } 178 | 179 | func envPlusLcAll() []string { 180 | env := os.Environ() 181 | return append(env, "LC_ALL=C") 182 | } 183 | -------------------------------------------------------------------------------- /mgotest_test.go: -------------------------------------------------------------------------------- 1 | package mgotest_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "gopkg.in/mgo.v2/bson" 7 | 8 | "github.com/facebookgo/mgotest" 9 | ) 10 | 11 | func test(t *testing.T, answer int) { 12 | t.Parallel() 13 | mongo := mgotest.NewStartedServer(t) 14 | defer mongo.Stop() 15 | const id = 1 16 | in := bson.M{"_id": id, "answer": answer} 17 | collection := mongo.Session().DB("tdb").C("tc") 18 | if err := collection.Insert(in); err != nil { 19 | t.Fatal(err) 20 | } 21 | out := bson.M{} 22 | if err := collection.FindId(id).One(out); err != nil { 23 | t.Fatal(err) 24 | } 25 | if out["answer"] != answer { 26 | t.Fatalf("did not find expected answer, got %v", out) 27 | } 28 | } 29 | 30 | // Testing that multiple instances don't stomp on each other. 31 | func TestOne(t *testing.T) { 32 | test(t, 42) 33 | } 34 | 35 | func TestTwo(t *testing.T) { 36 | test(t, 43) 37 | } 38 | 39 | func TestThree(t *testing.T) { 40 | test(t, 44) 41 | } 42 | 43 | func TestTestCommands(t *testing.T) { 44 | mongo := mgotest.NewStartedServer(t) 45 | defer mongo.Stop() 46 | session := mongo.Session() 47 | defer session.Close() 48 | command := bson.D{ 49 | {Name: "sleep", Value: 1}, 50 | {Name: "secs", Value: 1}, 51 | } 52 | if err := session.DB("admin").Run(command, nil); err != nil { 53 | t.Fatal(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /mgotestrs/mgotestrs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | 10 | "github.com/facebookgo/mgotest" 11 | ) 12 | 13 | func main() { 14 | l := log.New(os.Stdout, "", log.LstdFlags) 15 | n := flag.Uint("n", 3, "num nodes") 16 | flag.Parse() 17 | rs := mgotest.NewReplicaSet(*n, l) 18 | fmt.Println(rs.Addrs()) 19 | c := make(chan os.Signal, 1) 20 | signal.Notify(c, os.Interrupt) 21 | <-c 22 | rs.Stop() 23 | } 24 | -------------------------------------------------------------------------------- /patents: -------------------------------------------------------------------------------- 1 | Additional Grant of Patent Rights Version 2 2 | 3 | "Software" means the mgotest software distributed by Facebook, Inc. 4 | 5 | Facebook, Inc. ("Facebook") hereby grants to each recipient of the Software 6 | ("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable 7 | (subject to the termination provision below) license under any Necessary 8 | Claims, to make, have made, use, sell, offer to sell, import, and otherwise 9 | transfer the Software. For avoidance of doubt, no license is granted under 10 | Facebook’s rights in any patent claims that are infringed by (i) modifications 11 | to the Software made by you or any third party or (ii) the Software in 12 | combination with any software or other technology. 13 | 14 | The license granted hereunder will terminate, automatically and without notice, 15 | if you (or any of your subsidiaries, corporate affiliates or agents) initiate 16 | directly or indirectly, or take a direct financial interest in, any Patent 17 | Assertion: (i) against Facebook or any of its subsidiaries or corporate 18 | affiliates, (ii) against any party if such Patent Assertion arises in whole or 19 | in part from any software, technology, product or service of Facebook or any of 20 | its subsidiaries or corporate affiliates, or (iii) against any party relating 21 | to the Software. Notwithstanding the foregoing, if Facebook or any of its 22 | subsidiaries or corporate affiliates files a lawsuit alleging patent 23 | infringement against you in the first instance, and you respond by filing a 24 | patent infringement counterclaim in that lawsuit against that party that is 25 | unrelated to the Software, the license granted hereunder will not terminate 26 | under section (i) of this paragraph due to such counterclaim. 27 | 28 | A "Necessary Claim" is a claim of a patent owned by Facebook that is 29 | necessarily infringed by the Software standing alone. 30 | 31 | A "Patent Assertion" is any lawsuit or other action alleging direct, indirect, 32 | or contributory infringement or inducement to infringe any patent, including a 33 | cross-claim or counterclaim. 34 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | mgotest [![Build Status](https://secure.travis-ci.org/facebookgo/mgotest.png)](http://travis-ci.org/facebookgo/mgotest) 2 | ============= 3 | 4 | Documentation: http://godoc.org/github.com/facebookgo/mgotest 5 | -------------------------------------------------------------------------------- /rs.go: -------------------------------------------------------------------------------- 1 | package mgotest 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "gopkg.in/mgo.v2" 8 | "gopkg.in/mgo.v2/bson" 9 | ) 10 | 11 | const ( 12 | statePrimary = 1 13 | stateSecondary = 2 14 | ) 15 | 16 | type configMember struct { 17 | ID int `bson:"_id"` 18 | Host string `bson:"host"` 19 | } 20 | 21 | type config struct { 22 | ID string `bson:"_id"` 23 | Version int `bson:"version,omitempty"` 24 | Members []configMember `bson:"members"` 25 | } 26 | 27 | type statusMember struct { 28 | Name string `bson:"name"` 29 | State uint `bson:"state"` 30 | } 31 | 32 | type status struct { 33 | ID string `bson:"_id"` 34 | Set string `bson:"set"` 35 | MyState uint `bson:"myState"` 36 | Members []statusMember `bson:"members"` 37 | } 38 | 39 | // ReplicaSet provides a configured ReplicaSet. 40 | type ReplicaSet struct { 41 | T Fatalf 42 | Servers []*Server 43 | } 44 | 45 | // Stop the ReplicaSet. 46 | func (r *ReplicaSet) Stop() { 47 | var wg sync.WaitGroup 48 | wg.Add(len(r.Servers)) 49 | for _, s := range r.Servers { 50 | go func(s *Server) { 51 | defer wg.Done() 52 | s.Stop() 53 | }(s) 54 | } 55 | wg.Wait() 56 | } 57 | 58 | // Addrs for all the servers. 59 | func (r *ReplicaSet) Addrs() []string { 60 | var addrs []string 61 | for _, s := range r.Servers { 62 | addrs = append(addrs, s.URL()) 63 | } 64 | return addrs 65 | } 66 | 67 | // Session for the mongo ReplicaSet. 68 | func (r *ReplicaSet) Session() *mgo.Session { 69 | info := mgo.DialInfo{ 70 | Addrs: r.Addrs(), 71 | Timeout: 10 * time.Second, 72 | } 73 | session, err := mgo.DialWithInfo(&info) 74 | if err != nil { 75 | r.T.Fatalf(err.Error()) 76 | } 77 | return session 78 | } 79 | 80 | // NewReplicaSet makes a new ReplicaSet with the given number of nodes. 81 | func NewReplicaSet(num uint, tb Fatalf) *ReplicaSet { 82 | if num == 0 { 83 | tb.Fatalf("NewReplSet called with num=0") 84 | } 85 | 86 | rs := ReplicaSet{T: tb} 87 | for i := uint(0); i < num; i++ { 88 | rs.Servers = append(rs.Servers, NewReplSetServer(tb)) 89 | } 90 | 91 | var members []configMember 92 | for i, s := range rs.Servers { 93 | members = append(members, configMember{ 94 | ID: i, 95 | Host: s.URL(), 96 | }) 97 | } 98 | 99 | primary, err := mgo.Dial(rs.Servers[0].URL() + "?connect=direct") 100 | if err != nil { 101 | tb.Fatalf(err.Error()) 102 | } 103 | primary.SetMode(mgo.Monotonic, true) 104 | 105 | var initiateResponse bson.M 106 | err = primary.Run( 107 | bson.M{ 108 | "replSetInitiate": config{ 109 | ID: "rs", 110 | Members: []configMember{ 111 | {ID: int(0), Host: rs.Servers[0].URL()}, 112 | }, 113 | }, 114 | }, 115 | &initiateResponse, 116 | ) 117 | if err != nil { 118 | tb.Fatalf(err.Error()) 119 | } 120 | 121 | var statusResponse status 122 | for { 123 | err = primary.Run( 124 | bson.M{ 125 | "replSetGetStatus": int(1), 126 | }, 127 | &statusResponse, 128 | ) 129 | if err != nil { 130 | if err.Error() == "Received replSetInitiate - should come online shortly." { 131 | time.Sleep(time.Second) 132 | continue 133 | } 134 | tb.Fatalf(err.Error()) 135 | } 136 | if statusResponse.MyState != statePrimary { 137 | time.Sleep(time.Second) 138 | continue 139 | } 140 | break 141 | } 142 | 143 | var reconfigResponse bson.M 144 | err = primary.Run( 145 | bson.M{ 146 | "replSetReconfig": config{ 147 | ID: "rs", 148 | Version: 2, 149 | Members: members, 150 | }, 151 | }, 152 | &reconfigResponse, 153 | ) 154 | if err != nil { 155 | tb.Fatalf(err.Error()) 156 | } 157 | 158 | outerStatusReadyCheckLoop: 159 | for { 160 | err = primary.Run( 161 | bson.M{ 162 | "replSetGetStatus": int(1), 163 | }, 164 | &statusResponse, 165 | ) 166 | if err != nil { 167 | tb.Fatalf(err.Error()) 168 | } 169 | for _, m := range statusResponse.Members { 170 | if m.State != statePrimary && m.State != stateSecondary { 171 | time.Sleep(time.Second) 172 | continue outerStatusReadyCheckLoop 173 | } 174 | } 175 | break 176 | } 177 | 178 | return &rs 179 | } 180 | --------------------------------------------------------------------------------