1
1
package dbtestutil
2
2
3
3
import (
4
+ "bytes"
4
5
"context"
5
6
"database/sql"
6
7
"fmt"
7
8
"net/url"
8
9
"os"
10
+ "os/exec"
11
+ "path/filepath"
12
+ "regexp"
9
13
"strings"
10
14
"testing"
15
+ "time"
11
16
12
17
"github.com/stretchr/testify/require"
18
+ "golang.org/x/xerrors"
13
19
14
20
"github.com/coder/coder/v2/coderd/database"
15
21
"github.com/coder/coder/v2/coderd/database/dbfake"
@@ -24,6 +30,7 @@ func WillUsePostgres() bool {
24
30
25
31
type options struct {
26
32
fixedTimezone string
33
+ dumpOnFailure bool
27
34
}
28
35
29
36
type Option func (* options )
@@ -35,6 +42,13 @@ func WithTimezone(tz string) Option {
35
42
}
36
43
}
37
44
45
+ // WithDumpOnFailure will dump the entire database on test failure.
46
+ func WithDumpOnFailure () Option {
47
+ return func (o * options ) {
48
+ o .dumpOnFailure = true
49
+ }
50
+ }
51
+
38
52
func NewDB (t testing.TB , opts ... Option ) (database.Store , pubsub.Pubsub ) {
39
53
t .Helper ()
40
54
@@ -74,6 +88,9 @@ func NewDB(t testing.TB, opts ...Option) (database.Store, pubsub.Pubsub) {
74
88
t .Cleanup (func () {
75
89
_ = sqlDB .Close ()
76
90
})
91
+ if o .dumpOnFailure {
92
+ t .Cleanup (func () { DumpOnFailure (t , connectionURL ) })
93
+ }
77
94
db = database .New (sqlDB )
78
95
79
96
ps , err = pubsub .New (context .Background (), sqlDB , connectionURL )
@@ -110,3 +127,87 @@ func dbNameFromConnectionURL(t testing.TB, connectionURL string) string {
110
127
require .NoError (t , err )
111
128
return strings .TrimPrefix (u .Path , "/" )
112
129
}
130
+
131
+ // DumpOnFailure exports the database referenced by connectionURL to a file
132
+ // corresponding to the current test, with a suffix indicating the time the
133
+ // test was run.
134
+ // To import this into a new database (assuming you have already run make test-postgres-docker):
135
+ // - Create a new test database:
136
+ // go run ./scripts/migrate-ci/main.go and note the database name it outputs
137
+ // - Import the file into the above database:
138
+ // psql 'postgres://postgres:postgres@127.0.0.1:5432/<dbname>?sslmode=disable' -f <path to file.test.sql>
139
+ // - Run a dev server against that database:
140
+ // ./scripts/coder-dev.sh server --postgres-url='postgres://postgres:postgres@127.0.0.1:5432/<dbname>?sslmode=disable'
141
+ func DumpOnFailure (t testing.TB , connectionURL string ) {
142
+ if ! t .Failed () {
143
+ return
144
+ }
145
+ cwd , err := filepath .Abs ("." )
146
+ if err != nil {
147
+ t .Errorf ("dump on failure: cannot determine current working directory" )
148
+ return
149
+ }
150
+ snakeCaseName := regexp .MustCompile ("[^a-zA-Z0-9-_]+" ).ReplaceAllString (t .Name (), "_" )
151
+ now := time .Now ()
152
+ timeSuffix := fmt .Sprintf ("%d%d%d%d%d%d" , now .Year (), now .Month (), now .Day (), now .Hour (), now .Minute (), now .Second ())
153
+ outPath := filepath .Join (cwd , snakeCaseName + "." + timeSuffix + ".test.sql" )
154
+ dump , err := pgDump (connectionURL )
155
+ if err != nil {
156
+ t .Errorf ("dump on failure: failed to run pg_dump" )
157
+ return
158
+ }
159
+ if err := os .WriteFile (outPath , filterDump (dump ), 0o600 ); err != nil {
160
+ t .Errorf ("dump on failure: failed to write: %s" , err .Error ())
161
+ return
162
+ }
163
+ t .Logf ("Dumped database to %q due to failed test. I hope you find what you're looking for!" , outPath )
164
+ }
165
+
166
+ // pgDump runs pg_dump against dbURL and returns the output.
167
+ func pgDump (dbURL string ) ([]byte , error ) {
168
+ if _ , err := exec .LookPath ("pg_dump" ); err != nil {
169
+ return nil , xerrors .Errorf ("could not find pg_dump in path: %w" , err )
170
+ }
171
+ cmdArgs := []string {
172
+ "pg_dump" ,
173
+ dbURL ,
174
+ "--data-only" ,
175
+ "--column-inserts" ,
176
+ "--no-comments" ,
177
+ "--no-privileges" ,
178
+ "--no-publication" ,
179
+ "--no-security-labels" ,
180
+ "--no-subscriptions" ,
181
+ "--no-tablespaces" ,
182
+ // "--no-unlogged-table-data", // some tables are unlogged and may contain data of interest
183
+ "--no-owner" ,
184
+ "--exclude-table=schema_migrations" ,
185
+ }
186
+ cmd := exec .Command (cmdArgs [0 ], cmdArgs [1 :]... ) // nolint:gosec
187
+ cmd .Env = []string {
188
+ // "PGTZ=UTC", // This is probably not going to be useful if tz has been changed.
189
+ "PGCLIENTENCODINDG=UTF8" ,
190
+ "PGDATABASE=" , // we should always specify the database name in the connection string
191
+ }
192
+ var stdout bytes.Buffer
193
+ cmd .Stdout = & stdout
194
+ if err := cmd .Run (); err != nil {
195
+ return nil , xerrors .Errorf ("exec pg_dump: %w" , err )
196
+ }
197
+ return stdout .Bytes (), nil
198
+ }
199
+
200
+ func filterDump (dump []byte ) []byte {
201
+ lines := bytes .Split (dump , []byte {'\n' })
202
+ var buf bytes.Buffer
203
+ for _ , line := range lines {
204
+ // We dump in column-insert format, so these are the only lines
205
+ // we care about
206
+ if ! bytes .HasPrefix (line , []byte ("INSERT" )) {
207
+ continue
208
+ }
209
+ _ , _ = buf .Write (line )
210
+ _ , _ = buf .WriteRune ('\n' )
211
+ }
212
+ return buf .Bytes ()
213
+ }
0 commit comments