|
3 | 3 | package migrations_test |
4 | 4 |
|
5 | 5 | import ( |
| 6 | +"context" |
6 | 7 | "database/sql" |
7 | 8 | "fmt" |
| 9 | +"os" |
| 10 | +"path/filepath" |
| 11 | +"sync" |
8 | 12 | "testing" |
9 | 13 |
|
| 14 | +"github.com/golang-migrate/migrate/v4" |
| 15 | +migratepostgres"github.com/golang-migrate/migrate/v4/database/postgres" |
10 | 16 | "github.com/golang-migrate/migrate/v4/source" |
| 17 | +"github.com/golang-migrate/migrate/v4/source/iofs" |
11 | 18 | "github.com/golang-migrate/migrate/v4/source/stub" |
| 19 | +"github.com/lib/pq" |
12 | 20 | "github.com/stretchr/testify/require" |
13 | 21 | "go.uber.org/goleak" |
| 22 | +"golang.org/x/exp/slices" |
14 | 23 |
|
15 | 24 | "github.com/coder/coder/coderd/database/migrations" |
16 | 25 | "github.com/coder/coder/coderd/database/postgres" |
| 26 | +"github.com/coder/coder/testutil" |
17 | 27 | ) |
18 | 28 |
|
19 | 29 | funcTestMain(m*testing.M) { |
@@ -129,3 +139,196 @@ func TestCheckLatestVersion(t *testing.T) { |
129 | 139 | }) |
130 | 140 | } |
131 | 141 | } |
| 142 | + |
| 143 | +funcsetupMigrate(t*testing.T,db*sql.DB,name,pathstring) (source.Driver,*migrate.Migrate) { |
| 144 | +t.Helper() |
| 145 | + |
| 146 | +ctx:=context.Background() |
| 147 | + |
| 148 | +conn,err:=db.Conn(ctx) |
| 149 | +require.NoError(t,err) |
| 150 | + |
| 151 | +dbDriver,err:=migratepostgres.WithConnection(ctx,conn,&migratepostgres.Config{ |
| 152 | +MigrationsTable:"test_migrate_"+name, |
| 153 | +}) |
| 154 | +require.NoError(t,err) |
| 155 | + |
| 156 | +dirFS:=os.DirFS(path) |
| 157 | +d,err:=iofs.New(dirFS,".") |
| 158 | +require.NoError(t,err) |
| 159 | +t.Cleanup(func() { |
| 160 | +d.Close() |
| 161 | +}) |
| 162 | + |
| 163 | +m,err:=migrate.NewWithInstance(name,d,"",dbDriver) |
| 164 | +require.NoError(t,err) |
| 165 | +t.Cleanup(func() { |
| 166 | +m.Close() |
| 167 | +}) |
| 168 | + |
| 169 | +returnd,m |
| 170 | +} |
| 171 | + |
| 172 | +typetableStatsstruct { |
| 173 | +mu sync.Mutex |
| 174 | +smap[string]int |
| 175 | +} |
| 176 | + |
| 177 | +func (s*tableStats)Add(tablestring,nint) { |
| 178 | +s.mu.Lock() |
| 179 | +defers.mu.Unlock() |
| 180 | + |
| 181 | +s.s[table]=s.s[table]+n |
| 182 | +} |
| 183 | + |
| 184 | +func (s*tableStats)Empty() []string { |
| 185 | +s.mu.Lock() |
| 186 | +defers.mu.Unlock() |
| 187 | + |
| 188 | +varm []string |
| 189 | +fortable,n:=ranges.s { |
| 190 | +ifn==0 { |
| 191 | +m=append(m,table) |
| 192 | +} |
| 193 | +} |
| 194 | +returnm |
| 195 | +} |
| 196 | + |
| 197 | +funcTestMigrateUpWithFixtures(t*testing.T) { |
| 198 | +t.Parallel() |
| 199 | + |
| 200 | +iftesting.Short() { |
| 201 | +t.Skip() |
| 202 | +return |
| 203 | +} |
| 204 | + |
| 205 | +typetestCasestruct { |
| 206 | +namestring |
| 207 | +pathstring |
| 208 | + |
| 209 | +// For determining if test case table stats |
| 210 | +// are used to determine test coverage. |
| 211 | +useStatsbool |
| 212 | +} |
| 213 | +tests:= []testCase{ |
| 214 | +{ |
| 215 | +name:"fixtures", |
| 216 | +path:filepath.Join("testdata","fixtures"), |
| 217 | +useStats:true, |
| 218 | +}, |
| 219 | +// More test cases added via glob below. |
| 220 | +} |
| 221 | + |
| 222 | +// Folders in testdata/full_dumps represent fixtures for a full |
| 223 | +// deployment of Coder. |
| 224 | +matches,err:=filepath.Glob(filepath.Join("testdata","full_dumps","*")) |
| 225 | +require.NoError(t,err) |
| 226 | +for_,match:=rangematches { |
| 227 | +tests=append(tests,testCase{ |
| 228 | +name:filepath.Base(match), |
| 229 | +path:match, |
| 230 | +useStats:true, |
| 231 | +}) |
| 232 | +} |
| 233 | + |
| 234 | +// These tables are allowed to have zero rows for now, |
| 235 | +// but we should eventually add fixtures for them. |
| 236 | +ignoredTablesForStats:= []string{ |
| 237 | +"audit_logs", |
| 238 | +"git_auth_links", |
| 239 | +"group_members", |
| 240 | +"licenses", |
| 241 | +"replicas", |
| 242 | +} |
| 243 | +s:=&tableStats{s:make(map[string]int)} |
| 244 | + |
| 245 | +// This will run after all subtests have run and fail the test if |
| 246 | +// new tables have been added without covering them with fixtures. |
| 247 | +t.Cleanup(func() { |
| 248 | +emptyTables:=s.Empty() |
| 249 | +slices.Sort(emptyTables) |
| 250 | +for_,table:=rangeignoredTablesForStats { |
| 251 | +i:=slices.Index(emptyTables,table) |
| 252 | +ifi>=0 { |
| 253 | +emptyTables=slices.Delete(emptyTables,i,i+1) |
| 254 | +} |
| 255 | +} |
| 256 | +iflen(emptyTables)>0 { |
| 257 | +t.Logf("The following tables have zero rows, consider adding fixtures for them or create a full database dump:") |
| 258 | +t.Errorf("tables have zero rows: %v",emptyTables) |
| 259 | +t.Logf("See https://github.com/coder/coder/blob/main/docs/CONTRIBUTING.md#database-fixtures-for-testing-migrations for more information") |
| 260 | +} |
| 261 | +}) |
| 262 | + |
| 263 | +for_,tt:=rangetests { |
| 264 | +tt:=tt |
| 265 | + |
| 266 | +t.Run(tt.name,func(t*testing.T) { |
| 267 | +t.Parallel() |
| 268 | + |
| 269 | +db:=testSQLDB(t) |
| 270 | + |
| 271 | +ctx,_:=testutil.Context(t) |
| 272 | + |
| 273 | +// Prepare database for stepping up. |
| 274 | +err:=migrations.Down(db) |
| 275 | +require.NoError(t,err) |
| 276 | + |
| 277 | +// Initialize migrations for fixtures. |
| 278 | +fDriver,fMigrate:=setupMigrate(t,db,tt.name,tt.path) |
| 279 | + |
| 280 | +nextStep,err:=migrations.Stepper(db) |
| 281 | +require.NoError(t,err) |
| 282 | + |
| 283 | +varfixtureVeruint |
| 284 | +nextFixtureVer,err:=fDriver.First() |
| 285 | +require.NoError(t,err) |
| 286 | + |
| 287 | +for { |
| 288 | +version,more,err:=nextStep() |
| 289 | +require.NoError(t,err) |
| 290 | + |
| 291 | +if!more { |
| 292 | +// We reached the end of the migrations. |
| 293 | +break |
| 294 | +} |
| 295 | + |
| 296 | +ifnextFixtureVer==version { |
| 297 | +err=fMigrate.Steps(1) |
| 298 | +require.NoError(t,err) |
| 299 | +fixtureVer=version |
| 300 | + |
| 301 | +nv,_:=fDriver.Next(nextFixtureVer) |
| 302 | +ifnv>0 { |
| 303 | +nextFixtureVer=nv |
| 304 | +} |
| 305 | +} |
| 306 | + |
| 307 | +t.Logf("migrated to version %d, fixture version %d",version,fixtureVer) |
| 308 | +} |
| 309 | + |
| 310 | +// Gather number of rows for all existing tables |
| 311 | +// at the end of the migrations and fixtures. |
| 312 | +vartables pq.StringArray |
| 313 | +err=db.QueryRowContext(ctx,` |
| 314 | +SELECT array_agg(tablename) |
| 315 | +FROM pg_catalog.pg_tables |
| 316 | +WHERE |
| 317 | +schemaname != 'information_schema' |
| 318 | +AND schemaname != 'pg_catalog' |
| 319 | +AND tablename NOT LIKE 'test_migrate_%' |
| 320 | +`).Scan(&tables) |
| 321 | +require.NoError(t,err) |
| 322 | + |
| 323 | +for_,table:=rangetables { |
| 324 | +varcountint |
| 325 | +err=db.QueryRowContext(ctx,"SELECT COUNT(*) FROM "+table).Scan(&count) |
| 326 | +require.NoError(t,err) |
| 327 | + |
| 328 | +iftt.useStats { |
| 329 | +s.Add(table,count) |
| 330 | +} |
| 331 | +} |
| 332 | +}) |
| 333 | +} |
| 334 | +} |