blake.io/pqx@v0.2.2-0.20231231055241-83f2254c0a07/pqxtest/pqxtest.go (about)

     1  // Package pqxtest provides functions for testing with an embedded live,
     2  // lightweight, standalone Postgres instance optimized for fast setup/teardown
     3  // and cleanup.
     4  //
     5  // Starting, creating a database, using the database, and shutting down can all
     6  // be done with:
     7  //
     8  //	func TestMain(m *testing.M) {
     9  //		pqxtest.TestMain(m)
    10  //	}
    11  //
    12  //	func TestSomething(t *testing.T) {
    13  //		db := pqxtest.CreateDB(t, "CREATE TABLE foo (id INT)")
    14  //		// ... do something with db ...
    15  //		// NOTE: db will be closed automatically when t.Cleanup is called and the database will be dropped.
    16  //	}
    17  //
    18  // # Developer Speed
    19  //
    20  // Pqxtest enables a developer to go from zero to well tested, production ready
    21  // code that interacts with a database in no time. This means removing much of
    22  // the toil associated with configuring and running a standalone postgres
    23  // instance, the can be brittle is left standing across test runs,
    24  // over engineering with abundant interfaces for mocking.
    25  //
    26  // # Easy CI
    27  //
    28  // Because pqxtest uses pqx which "embeds" a postgres into you application, CI
    29  // is just "go test". No extra environment setup required. See this packages
    30  // .github/workflows for an example.
    31  //
    32  // # No Mocks
    33  //
    34  // Mocking database interactions is dangerous. It is easy to create a test that
    35  // gives false positives, or can cause developers precious time hunting down a
    36  // bug that doesn't exist because they're getting false negatives from the mocks
    37  // (i.e. the code would work if running against an actual database).
    38  //
    39  // Writing mocks and mocking code is almost always avoidable when interacting
    40  // with a live database. Try pqxtest and ditch you mocking libraries and all
    41  // the for-tests-only interfaces and use concrete types instead!
    42  //
    43  // # No global transaction
    44  //
    45  // Some test libraries create a single transaction for a test and return a
    46  // wrapper-driver *sql.DB that runs all queries in a test in that transaction.
    47  // This is less than ideal because, like mocks, this isn't how applications work
    48  // in production, so  it can lead to weird behaviors and false positives. It
    49  // also means you can't use those nifty driver specific functions you love.
    50  // Pqxtest returns a real *sql.DB.
    51  //
    52  // # Speed
    53  //
    54  // Pqx is fast. It is designed to give you all the benefits of writing tests
    55  // against a real database without slowing you down. That means no waiting for
    56  // setup and teardown after the first test run. The first "go test" takes only a
    57  // second or two to cache the standalone postgres binary and initialize the data
    58  // directory, but subsequent runs skip these steps, making them very fast!
    59  //
    60  // # Logs
    61  //
    62  // Databases are created using the test name, and all logs associated with the
    63  // test database are logged directly to the test t.Logf function. This provides
    64  // rapid feedback and context when things go wrong. No more greping around in a
    65  // shared log file.
    66  //
    67  // For example, the following failing tests will log the postgres logs for their
    68  // databases only:
    69  //
    70  //	func TestFailingInsert(t *testing.T) {
    71  //		db := pqxtest.CreateDB(t, ``)
    72  //		_, err := db.Exec(`INSERT INTO foo VALUES (1)`)
    73  //		if err != nil {
    74  //			t.Fatal(err)
    75  //		}
    76  //	}
    77  //
    78  //	func TestFailingSelect(t *testing.T) {
    79  //		db := pqxtest.CreateDB(t, ``)
    80  //		_, err := db.Exec(`SELECT * FROM bar`)
    81  //		if err != nil {
    82  //			t.Fatal(err)
    83  //		}
    84  //	}
    85  //
    86  // Running ("go test") will produce:
    87  //
    88  //	--- FAIL: TestFailingInsert (0.03s)
    89  //	    example_test.go:54: [pqx]: psql 'host=localhost port=51718 dbname=testfailinginsert_37d0bb55e5c8cb86 sslmode=disable'
    90  //	    logplex.go:123: ERROR:  relation "foo" does not exist at character 13
    91  //	    logplex.go:123: STATEMENT:  INSERT INTO foo VALUES (1)
    92  //	    example_test.go:57: pq: relation "foo" does not exist
    93  //	--- FAIL: TestFailingSelect (0.03s)
    94  //	    example_test.go:62: [pqx]: psql 'host=localhost port=51718 dbname=testfailingselect_2649dda9b27d8c74 sslmode=disable'
    95  //	    logplex.go:123: ERROR:  relation "bar" does not exist at character 15
    96  //	    logplex.go:123: STATEMENT:  SELECT * FROM bar
    97  //	    example_test.go:65: pq: relation "bar" does not exist
    98  //
    99  // Tip: Try running these tests with "go test -v -pqxtest.d=2" to see more detailed logs in the
   100  // tests, or set it to 3 and see even more verbose logs.
   101  //
   102  // # PSQL
   103  //
   104  // Test databases can be accessed using the psql command line tool before they
   105  // exist. Use the BlockForPSQL function to accomplish this.
   106  //
   107  // # No Config
   108  //
   109  // Pqx starts each postgres with reasonable defaults for the most common use
   110  // cases. The defaults can be overridden by setting environment variables and
   111  // using flags. See below.
   112  //
   113  // # Environment Variables
   114  //
   115  // The following environment variables are recognized:
   116  //
   117  //	PQX_PG_VERSION: Specifies the version of postgres to use. The default is pqx.DefaultVersion.
   118  //
   119  // # Flags
   120  //
   121  // pqxtest recognizes the following flag:
   122  //
   123  //	-pqxtest.d=<level>: Sets the debug level for the Postgres instance. See Logs for more details.
   124  //
   125  // Flags may be specified with go test like:
   126  //
   127  //	go test -v -pqxtest.d=2
   128  package pqxtest
   129  
   130  import (
   131  	"bytes"
   132  	"context"
   133  	"crypto/rand"
   134  	"database/sql"
   135  	"flag"
   136  	"fmt"
   137  	"io"
   138  	"log"
   139  	"os"
   140  	"os/exec"
   141  	"path/filepath"
   142  	"strconv"
   143  	"strings"
   144  	"sync"
   145  	"syscall"
   146  	"testing"
   147  	"time"
   148  	"unicode"
   149  
   150  	"blake.io/pqx"
   151  	"blake.io/pqx/internal/logplex"
   152  )
   153  
   154  // Flags
   155  var (
   156  	flagDebugLevel = flag.Int("pqxtest.d", 0, "postgres debug level (see `postgres -d`)")
   157  )
   158  
   159  var (
   160  	sharedPG *pqx.Postgres
   161  
   162  	dmu  sync.Mutex
   163  	dsns = map[testing.TB][]string{}
   164  )
   165  
   166  // DSN returns the main dsn for the running postgres instance. It must only be
   167  // call after a call to Start.
   168  func DSN() string {
   169  	return sharedPG.DSN("postgres")
   170  }
   171  
   172  // DSNForTest returns the dsn for the first test database created using t. It
   173  // must only be called after a call to CreateDB.
   174  func DSNForTest(t testing.TB) string {
   175  	dmu.Lock()
   176  	defer dmu.Unlock()
   177  	return dsns[t][0]
   178  }
   179  
   180  // TestMain is a convenience function for running tests with a live Postgres
   181  // instance. It starts the Postgres instance before calling m.Run, and then
   182  // calls Shutdown after.
   183  //
   184  // Users that need do more in their TestMain, can use it as a reference.
   185  func TestMain(m *testing.M) {
   186  	flag.Parse()
   187  	Start(5*time.Second, *flagDebugLevel)
   188  	defer Shutdown() //nolint
   189  	code := m.Run()
   190  	Shutdown()
   191  	os.Exit(code)
   192  }
   193  
   194  // Start starts a Postgres instance. The version used is determined by the
   195  // PQX_PG_VERSION environment variable if set, otherwise pqx.DefaultVersion is
   196  // used.
   197  //
   198  // The Postgres instance is started in a temporary directory named after the
   199  // current working directory and reused across runs.
   200  func Start(timeout time.Duration, debugLevel int) {
   201  	maybeBecomeSupervisor()
   202  
   203  	sharedPG = &pqx.Postgres{
   204  		Version:    os.Getenv("PQX_PG_VERSION"),
   205  		Dir:        getSharedDir(),
   206  		DebugLevel: debugLevel,
   207  	}
   208  
   209  	ctx, cancel := context.WithTimeout(context.Background(), timeout)
   210  	defer cancel()
   211  
   212  	startLog := new(lockedBuffer)
   213  	if err := sharedPG.Start(ctx, logplex.LogfFromWriter(startLog)); err != nil {
   214  		if _, err := startLog.WriteTo(os.Stderr); err != nil {
   215  			log.Fatalf("error writing start log: %v", err)
   216  		}
   217  		log.Fatalf("error starting Postgres: %v", err)
   218  	}
   219  
   220  	shutThisDownAfterMyDeath(sharedPG.Pid())
   221  }
   222  
   223  // Shutdown shuts down the shared Postgres instance.
   224  func Shutdown() {
   225  	if sharedPG == nil {
   226  		return
   227  	}
   228  	if err := sharedPG.ShutdownAlone(); err != nil {
   229  		log.Printf("error shutting down Postgres: %v", err)
   230  	}
   231  }
   232  
   233  // CreateDB creates and returns a database using the shared Postgres instance.
   234  // The database will automatically be cleaned up just before the test ends.
   235  //
   236  // All logs associated with the database will be written to t.Logf.
   237  func CreateDB(t testing.TB, schema string) *sql.DB {
   238  	t.Helper()
   239  	if sharedPG == nil {
   240  		t.Fatal("pqxtest.TestMain not called")
   241  	}
   242  	t.Cleanup(func() {
   243  		sharedPG.Flush()
   244  	})
   245  
   246  	name := cleanName(t.Name())
   247  	name = fmt.Sprintf("%s_%s", name, randomString())
   248  	db, dsn, cleanup, err := sharedPG.CreateDB(context.Background(), t.Logf, name, schema)
   249  	if err != nil {
   250  		t.Fatal(err)
   251  	}
   252  	t.Cleanup(func() {
   253  		cleanup()
   254  		dmu.Lock()
   255  		delete(dsns, t)
   256  		dmu.Unlock()
   257  	})
   258  
   259  	dmu.Lock()
   260  	dsns[t] = append(dsns[t], dsn)
   261  	dmu.Unlock()
   262  
   263  	return db
   264  }
   265  
   266  // BlockForPSQL logs the psql commands for connecting to all databases created
   267  // by CreateDB in a test, and blocks the current goroutine allowing the user to
   268  // interact with the databases.
   269  //
   270  // As a special case, if testing.Verbose is false, it logs to stderr to avoid
   271  // silently hanging the tests.
   272  //
   273  // BreakForPSQL is intended for debugging only and not to be left in tests.
   274  //
   275  // Example Usage:
   276  //
   277  //	 func TestSomething(t *testing.T) {
   278  //	 	db := pqxtest.CreateDB(t, "CREATE TABLE foo (id INT)")
   279  //		defer pqxtest.BlockForPSQL(t) // will run even in the face of t.Fatal/Fail.
   280  //	 	// ... do something with db ...
   281  //	 }
   282  func BlockForPSQL(t testing.TB) {
   283  	t.Helper()
   284  
   285  	logf := t.Logf
   286  	if !testing.Verbose() {
   287  		// Calling this function without -v means users will not see
   288  		// this message if we use t.Logf, and the tests will silently
   289  		// hang, which isn't ideal, so we ensure the users sees we're
   290  		// blocking.
   291  		logf = func(format string, args ...any) {
   292  			t.Helper()
   293  			fmt.Fprintf(os.Stderr, format+"\n", args...)
   294  		}
   295  	}
   296  
   297  	dmu.Lock()
   298  	dsns := dsns[t]
   299  	dmu.Unlock()
   300  
   301  	if len(dsns) == 0 {
   302  		logf("[pqx]: BlockForPSQL: no databases to interact with")
   303  	}
   304  
   305  	for _, dsn := range dsns {
   306  		logf("blocking for SQL; press Ctrl-C to quit")
   307  		logf("[pqx]: psql '%s'", dsn)
   308  	}
   309  	select {}
   310  }
   311  
   312  func getSharedDir() string {
   313  	cwd, err := os.Getwd()
   314  	if err != nil {
   315  		panic(err)
   316  	}
   317  	return filepath.Join(os.TempDir(), "pqx", cwd)
   318  }
   319  
   320  func cleanName(name string) string {
   321  	rr := []rune(name)
   322  	for i, r := range rr {
   323  		if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
   324  			rr[i] = '_'
   325  		}
   326  	}
   327  	return strings.ToLower(string(rr))
   328  }
   329  
   330  func randomString() string {
   331  	var buf [8]byte
   332  	if _, err := rand.Read(buf[:]); err != nil {
   333  		panic(err)
   334  	}
   335  	return fmt.Sprintf("%x", buf)
   336  }
   337  
   338  type lockedBuffer struct {
   339  	mu sync.Mutex
   340  	b  bytes.Buffer
   341  }
   342  
   343  func (b *lockedBuffer) Write(p []byte) (int, error) {
   344  	b.mu.Lock()
   345  	defer b.mu.Unlock()
   346  	return b.b.Write(p)
   347  }
   348  
   349  func (b *lockedBuffer) WriteTo(w io.Writer) (int64, error) {
   350  	b.mu.Lock()
   351  	defer b.mu.Unlock()
   352  	return b.b.WriteTo(w)
   353  }
   354  
   355  func maybeBecomeSupervisor() {
   356  	pid, _ := strconv.Atoi(os.Getenv("_PQX_SUP_PID"))
   357  	if pid == 0 {
   358  		return
   359  	}
   360  	log.SetFlags(0)
   361  	awaitParentDeath()
   362  	p, err := os.FindProcess(pid)
   363  	if err != nil {
   364  		log.Fatalf("find process: %v", err)
   365  	}
   366  	if err := p.Signal(syscall.Signal(syscall.SIGQUIT)); err != nil {
   367  		log.Fatalf("error signaling process: %v", err)
   368  	}
   369  	os.Exit(0)
   370  }
   371  
   372  func awaitParentDeath() {
   373  	_, _ = os.Stdin.Read(make([]byte, 1))
   374  }
   375  
   376  func shutThisDownAfterMyDeath(pid int) {
   377  	exe, err := os.Executable()
   378  	if err != nil {
   379  		panic(err)
   380  	}
   381  	sup := exec.Command(exe)
   382  	sup.Env = append(os.Environ(), "_PQX_SUP_PID="+strconv.Itoa(pid))
   383  	sup.Stdout = os.Stdout
   384  	sup.Stderr = os.Stderr
   385  
   386  	// set a pipe we never write to as to block the supervisor until we die
   387  	_, err = sup.StdinPipe()
   388  	if err != nil {
   389  		panic(err)
   390  	}
   391  	err = sup.Start()
   392  	if err != nil {
   393  		panic(err)
   394  	}
   395  
   396  	go func() {
   397  		// Exiting this function without this reference to sup means
   398  		// sup can become eligible for GC after this exists. This means
   399  		// the stdin pipe will also be collected, ultimately causing
   400  		// sup to think it's parent has died, and it will shutdown
   401  		// postgres and exit.
   402  		_ = sup.Wait()
   403  	}()
   404  }