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  }