github.com/mendersoftware/go-lib-micro@v0.0.0-20240304135804-e8e39c59b148/mongo/dbtest/db.go (about) 1 // Copyright 2023 Northern.tech AS 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Copyright 2010-2013 Gustavo Niemeyer <gustavo@niemeyer.net> 16 17 // mgo - MongoDB driver for Go 18 19 // Copyright (c) 2010-2013 - Gustavo Niemeyer <gustavo@niemeyer.net> 20 21 // All rights reserved. 22 23 // Redistribution and use in source and binary forms, with or without 24 // modification, are permitted provided that the following conditions are met: 25 26 // 1. Redistributions of source code must retain the above copyright notice, this 27 // list of conditions and the following disclaimer. 28 // 2. Redistributions in binary form must reproduce the above copyright notice, 29 // this list of conditions and the following disclaimer in the documentation 30 // and/or other materials provided with the distribution. 31 32 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 33 // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 34 // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 35 // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 36 // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 37 // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 38 // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 39 // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 40 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 41 // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 42 43 package dbtest 44 45 import ( 46 "bytes" 47 "context" 48 "fmt" 49 "net" 50 "os" 51 "os/exec" 52 "runtime" 53 "strconv" 54 "time" 55 56 "go.mongodb.org/mongo-driver/bson" 57 "go.mongodb.org/mongo-driver/mongo" 58 "go.mongodb.org/mongo-driver/mongo/options" 59 60 "gopkg.in/tomb.v2" 61 ) 62 63 // DBServer controls a MongoDB server process to be used within test suites. 64 // 65 // The test server is started when Client is called the first time and should 66 // remain running for the duration of all tests, with the Wipe method being 67 // called between tests (before each of them) to clear stored data. After all tests 68 // are done, the Stop method should be called to stop the test server. 69 // 70 // Before the DBServer is used the SetPath method must be called to define 71 // the location for the database files to be stored. 72 type DBServer struct { 73 Ctx context.Context 74 timeout time.Duration 75 client *mongo.Client 76 output bytes.Buffer 77 server *exec.Cmd 78 dbpath string 79 host string 80 tomb tomb.Tomb 81 } 82 83 // SetPath defines the path to the directory where the database files will be 84 // stored if it is started. The directory path itself is not created or removed 85 // by the test helper. 86 func (dbs *DBServer) SetPath(dbpath string) { 87 dbs.dbpath = dbpath 88 } 89 90 func (dbs *DBServer) SetTimeout(timeout int) { 91 dbs.timeout = time.Duration(timeout) 92 } 93 94 func (dbs *DBServer) start() { 95 if dbs.server != nil { 96 panic("DBServer already started") 97 } 98 if dbs.dbpath == "" { 99 panic("DBServer.SetPath must be called before using the server") 100 } 101 l, err := net.Listen("tcp", "127.0.0.1:0") 102 if err != nil { 103 panic("unable to listen on a local address: " + err.Error()) 104 } 105 addr := l.Addr().(*net.TCPAddr) 106 l.Close() 107 dbs.host = addr.String() 108 109 args := []string{ 110 "--dbpath", dbs.dbpath, 111 "--bind_ip", "127.0.0.1", 112 "--port", strconv.Itoa(addr.Port), 113 "--nojournal", 114 } 115 dbs.tomb = tomb.Tomb{} 116 dbs.server = exec.Command("mongod", args...) 117 dbs.server.Stdout = &dbs.output 118 dbs.server.Stderr = &dbs.output 119 err = dbs.server.Start() 120 if err != nil { 121 // print error to facilitate troubleshooting as the panic will be caught in a panic handler 122 fmt.Fprintf(os.Stderr, "mongod failed to start: %v\n", err) 123 panic(err) 124 } 125 dbs.tomb.Go(dbs.monitor) 126 dbs.Wipe() 127 } 128 129 func (dbs *DBServer) monitor() error { 130 dbs.server.Process.Wait() 131 if dbs.tomb.Alive() { 132 // Present some debugging information. 133 fmt.Fprintf(os.Stderr, "---- mongod process died unexpectedly:\n") 134 fmt.Fprintf(os.Stderr, "%s", dbs.output.Bytes()) 135 fmt.Fprintf(os.Stderr, "---- mongod processes running right now:\n") 136 cmd := exec.Command("/bin/sh", "-c", "ps auxw | grep mongod") 137 cmd.Stdout = os.Stderr 138 cmd.Stderr = os.Stderr 139 cmd.Run() 140 fmt.Fprintf(os.Stderr, "----------------------------------------\n") 141 142 panic("mongod process died unexpectedly") 143 } 144 return nil 145 } 146 147 // Stop stops the test server process, if it is running. 148 // 149 // It's okay to call Stop multiple times. After the test server is 150 // stopped it cannot be restarted. 151 // 152 // All database clients must be closed before or while the Stop method 153 // is running. Otherwise Stop will panic after a timeout informing that 154 // there is a client leak. 155 func (dbs *DBServer) Stop() { 156 if dbs.client != nil { 157 if dbs.client != nil { 158 dbs.client.Disconnect(dbs.Ctx) 159 dbs.client = nil 160 } 161 } 162 if dbs.server != nil { 163 dbs.tomb.Kill(nil) 164 // Windows doesn't support Interrupt 165 if runtime.GOOS == "windows" { 166 dbs.server.Process.Signal(os.Kill) 167 } else { 168 dbs.server.Process.Signal(os.Interrupt) 169 } 170 select { 171 case <-dbs.tomb.Dead(): 172 case <-time.After(5 * time.Second): 173 panic("timeout waiting for mongod process to die") 174 } 175 dbs.server = nil 176 } 177 } 178 179 // Client returns a new client to the server. The returned client 180 // must be disconnected after the tests are finished. 181 // 182 // The first call to Client will start the DBServer. 183 func (dbs *DBServer) Client() *mongo.Client { 184 if dbs.server == nil { 185 dbs.start() 186 } 187 if dbs.client == nil { 188 var err error 189 190 if dbs.timeout == 0 { 191 dbs.timeout = 8 192 } 193 clientOptions := options.Client().ApplyURI("mongodb://" + dbs.host + "/test") 194 dbs.Ctx = context.Background() // context.WithTimeout(context.Background(), dbs.timeout*time.Second) 195 dbs.client, err = mongo.Connect(dbs.Ctx, clientOptions) 196 if err != nil { 197 panic(err) 198 } 199 if dbs.client == nil { 200 panic("cant connect") 201 } 202 } 203 return dbs.client 204 } 205 206 func (dbs *DBServer) CTX() context.Context { 207 return dbs.Ctx 208 } 209 210 // Wipe drops all created databases and their data. 211 func (dbs *DBServer) Wipe() { 212 if dbs.server == nil || dbs.client == nil { 213 return 214 } 215 client := dbs.Client() 216 names, err := client.ListDatabaseNames(dbs.Ctx, bson.M{}) 217 if err != nil { 218 panic(err) 219 } 220 for _, name := range names { 221 switch name { 222 case "admin", "local", "config": 223 default: 224 err = dbs.client.Database(name).Drop(dbs.Ctx) 225 if err != nil { 226 panic(err) 227 } 228 } 229 } 230 }