github.com/stackdocker/rkt@v0.10.1-0.20151109095037-1aa827478248/store/migrate_test.go (about)

     1  // Copyright 2015 The rkt Authors
     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  package store
    16  
    17  import (
    18  	"database/sql"
    19  	"fmt"
    20  	"io/ioutil"
    21  	"os"
    22  	"path/filepath"
    23  	"reflect"
    24  	"testing"
    25  	"time"
    26  )
    27  
    28  type testdb interface {
    29  	version() int
    30  	populate(db *DB) error
    31  	load(db *DB) error
    32  	compare(db testdb) bool
    33  }
    34  
    35  type DBV0 struct {
    36  	aciinfos []*ACIInfoV0_2
    37  	remotes  []*RemoteV0_1
    38  }
    39  
    40  func (d *DBV0) version() int {
    41  	return 0
    42  }
    43  
    44  func (d *DBV0) populate(db *DB) error {
    45  	// As DBV0 and DBV1 have the same schema use a common populate
    46  	// function.
    47  	return populateDBV0_1(db, d.version(), d.aciinfos, d.remotes)
    48  }
    49  
    50  // load populates the given struct with the data in db.
    51  // the given struct d should be empty
    52  func (d *DBV0) load(db *DB) error {
    53  	fn := func(tx *sql.Tx) error {
    54  		var err error
    55  		d.aciinfos, err = getAllACIInfosV0_2(tx)
    56  		if err != nil {
    57  			return err
    58  		}
    59  		d.remotes, err = getAllRemoteV0_1(tx)
    60  		if err != nil {
    61  			return err
    62  		}
    63  		return nil
    64  	}
    65  	if err := db.Do(fn); err != nil {
    66  		return err
    67  	}
    68  	return nil
    69  }
    70  
    71  func (d *DBV0) compare(td testdb) bool {
    72  	d2, ok := td.(*DBV0)
    73  	if !ok {
    74  		return false
    75  	}
    76  	if !compareSlicesNoOrder(d.aciinfos, d2.aciinfos) {
    77  		return false
    78  	}
    79  	if !compareSlicesNoOrder(d.remotes, d2.remotes) {
    80  		return false
    81  	}
    82  	return true
    83  }
    84  
    85  type DBV1 struct {
    86  	aciinfos []*ACIInfoV0_2
    87  	remotes  []*RemoteV0_1
    88  }
    89  
    90  func (d *DBV1) version() int {
    91  	return 1
    92  }
    93  func (d *DBV1) populate(db *DB) error {
    94  	return populateDBV0_1(db, d.version(), d.aciinfos, d.remotes)
    95  }
    96  
    97  func (d *DBV1) load(db *DB) error {
    98  	fn := func(tx *sql.Tx) error {
    99  		var err error
   100  		d.aciinfos, err = getAllACIInfosV0_2(tx)
   101  		if err != nil {
   102  			return err
   103  		}
   104  		d.remotes, err = getAllRemoteV0_1(tx)
   105  		if err != nil {
   106  			return err
   107  		}
   108  		return nil
   109  	}
   110  	if err := db.Do(fn); err != nil {
   111  		return err
   112  	}
   113  	return nil
   114  }
   115  
   116  func (d *DBV1) compare(td testdb) bool {
   117  	d2, ok := td.(*DBV1)
   118  	if !ok {
   119  		return false
   120  	}
   121  	if !compareSlicesNoOrder(d.aciinfos, d2.aciinfos) {
   122  		return false
   123  	}
   124  	if !compareSlicesNoOrder(d.remotes, d2.remotes) {
   125  		return false
   126  	}
   127  	return true
   128  }
   129  
   130  type DBV2 struct {
   131  	aciinfos []*ACIInfoV0_2
   132  	remotes  []*RemoteV2_3
   133  }
   134  
   135  func (d *DBV2) version() int {
   136  	return 2
   137  }
   138  func (d *DBV2) populate(db *DB) error {
   139  	return populateDBV2(db, d.version(), d.aciinfos, d.remotes)
   140  }
   141  
   142  func (d *DBV2) load(db *DB) error {
   143  	fn := func(tx *sql.Tx) error {
   144  		var err error
   145  		d.aciinfos, err = getAllACIInfosV0_2(tx)
   146  		if err != nil {
   147  			return err
   148  		}
   149  		d.remotes, err = getAllRemoteV2_3(tx)
   150  		if err != nil {
   151  			return err
   152  		}
   153  		return nil
   154  	}
   155  	if err := db.Do(fn); err != nil {
   156  		return err
   157  	}
   158  	return nil
   159  }
   160  
   161  func (d *DBV2) compare(td testdb) bool {
   162  	d2, ok := td.(*DBV2)
   163  	if !ok {
   164  		return false
   165  	}
   166  	if !compareSlicesNoOrder(d.aciinfos, d2.aciinfos) {
   167  		return false
   168  	}
   169  	if !compareSlicesNoOrder(d.remotes, d2.remotes) {
   170  		return false
   171  	}
   172  	return true
   173  }
   174  
   175  type DBV3 struct {
   176  	aciinfos []*ACIInfoV3
   177  	remotes  []*RemoteV2_3
   178  }
   179  
   180  func (d *DBV3) version() int {
   181  	return 3
   182  }
   183  func (d *DBV3) populate(db *DB) error {
   184  	return populateDBV3(db, d.version(), d.aciinfos, d.remotes)
   185  }
   186  
   187  func (d *DBV3) load(db *DB) error {
   188  	fn := func(tx *sql.Tx) error {
   189  		var err error
   190  		d.aciinfos, err = getAllACIInfosV3(tx)
   191  		if err != nil {
   192  			return err
   193  		}
   194  		d.remotes, err = getAllRemoteV2_3(tx)
   195  		if err != nil {
   196  			return err
   197  		}
   198  		return nil
   199  	}
   200  	if err := db.Do(fn); err != nil {
   201  		return err
   202  	}
   203  	return nil
   204  }
   205  
   206  func (d *DBV3) compare(td testdb) bool {
   207  	d3, ok := td.(*DBV3)
   208  	if !ok {
   209  		return false
   210  	}
   211  	if !compareSlicesNoOrder(d.aciinfos, d3.aciinfos) {
   212  		return false
   213  	}
   214  	if !compareSlicesNoOrder(d.remotes, d3.remotes) {
   215  		return false
   216  	}
   217  	return true
   218  }
   219  
   220  // The ACIInfo struct for different db versions. The ending VX_Y represent the
   221  // first and the last version where the format isn't changed
   222  // The latest existing struct should be updated when updating the db version
   223  // without changing the struct format (ex. V0_1 to V0_2).
   224  // A new struct and its relative function should be added if the format is changed.
   225  // The same applies for all of the the other structs.
   226  type ACIInfoV0_2 struct {
   227  	BlobKey    string
   228  	AppName    string
   229  	ImportTime time.Time
   230  	Latest     bool
   231  }
   232  
   233  type ACIInfoV3 struct {
   234  	BlobKey    string
   235  	Name       string
   236  	ImportTime time.Time
   237  	Latest     bool
   238  }
   239  
   240  func getAllACIInfosV0_2(tx *sql.Tx) ([]*ACIInfoV0_2, error) {
   241  	var aciinfos []*ACIInfoV0_2
   242  	rows, err := tx.Query("SELECT * from aciinfo")
   243  	if err != nil {
   244  		return nil, err
   245  	}
   246  	for rows.Next() {
   247  		aciinfo := &ACIInfoV0_2{}
   248  		if err := rows.Scan(&aciinfo.BlobKey, &aciinfo.AppName, &aciinfo.ImportTime, &aciinfo.Latest); err != nil {
   249  			return nil, err
   250  		}
   251  		aciinfos = append(aciinfos, aciinfo)
   252  	}
   253  	if err := rows.Err(); err != nil {
   254  		return nil, err
   255  	}
   256  	return aciinfos, nil
   257  }
   258  
   259  func getAllACIInfosV3(tx *sql.Tx) ([]*ACIInfoV3, error) {
   260  	var aciinfos []*ACIInfoV3
   261  	rows, err := tx.Query("SELECT * from aciinfo")
   262  	if err != nil {
   263  		return nil, err
   264  	}
   265  	for rows.Next() {
   266  		aciinfo := &ACIInfoV3{}
   267  		if rows.Scan(&aciinfo.BlobKey, &aciinfo.Name, &aciinfo.ImportTime, &aciinfo.Latest); err != nil {
   268  			return nil, err
   269  		}
   270  		aciinfos = append(aciinfos, aciinfo)
   271  	}
   272  	if err := rows.Err(); err != nil {
   273  		return nil, err
   274  	}
   275  	return aciinfos, nil
   276  }
   277  
   278  type RemoteV0_1 struct {
   279  	ACIURL  string
   280  	SigURL  string
   281  	ETag    string
   282  	BlobKey string
   283  }
   284  
   285  func getAllRemoteV0_1(tx *sql.Tx) ([]*RemoteV0_1, error) {
   286  	var remotes []*RemoteV0_1
   287  	rows, err := tx.Query("SELECT * from remote")
   288  	if err != nil {
   289  		return nil, err
   290  	}
   291  	for rows.Next() {
   292  		remote := &RemoteV0_1{}
   293  		if err := rows.Scan(&remote.ACIURL, &remote.SigURL, &remote.ETag, &remote.BlobKey); err != nil {
   294  			return nil, err
   295  		}
   296  		remotes = append(remotes, remote)
   297  	}
   298  	if err := rows.Err(); err != nil {
   299  		return nil, err
   300  	}
   301  	return remotes, nil
   302  }
   303  
   304  type RemoteV2_3 struct {
   305  	ACIURL       string
   306  	SigURL       string
   307  	ETag         string
   308  	BlobKey      string
   309  	CacheMaxAge  int
   310  	DownloadTime time.Time
   311  }
   312  
   313  func getAllRemoteV2_3(tx *sql.Tx) ([]*RemoteV2_3, error) {
   314  	var remotes []*RemoteV2_3
   315  	rows, err := tx.Query("SELECT * from remote")
   316  	if err != nil {
   317  		return nil, err
   318  	}
   319  	for rows.Next() {
   320  		remote := &RemoteV2_3{}
   321  		if err := rows.Scan(&remote.ACIURL, &remote.SigURL, &remote.ETag, &remote.BlobKey, &remote.CacheMaxAge, &remote.DownloadTime); err != nil {
   322  			return nil, err
   323  		}
   324  		remotes = append(remotes, remote)
   325  	}
   326  	if err := rows.Err(); err != nil {
   327  		return nil, err
   328  	}
   329  	return remotes, nil
   330  }
   331  
   332  func populateDBV0_1(db *DB, dbVersion int, aciInfos []*ACIInfoV0_2, remotes []*RemoteV0_1) error {
   333  	var dbCreateStmts = [...]string{
   334  		// version table
   335  		"CREATE TABLE IF NOT EXISTS version (version int);",
   336  		fmt.Sprintf("INSERT INTO version VALUES (%d)", dbVersion),
   337  
   338  		// remote table. The primary key is "aciurl".
   339  		"CREATE TABLE IF NOT EXISTS remote (aciurl string, sigurl string, etag string, blobkey string);",
   340  		"CREATE UNIQUE INDEX IF NOT EXISTS aciurlidx ON remote (aciurl)",
   341  
   342  		// aciinfo table. The primary key is "blobkey" and it matches the key used to save that aci in the blob store
   343  		"CREATE TABLE IF NOT EXISTS aciinfo (blobkey string, appname string, importtime time, latest bool);",
   344  		"CREATE UNIQUE INDEX IF NOT EXISTS blobkeyidx ON aciinfo (blobkey)",
   345  		"CREATE INDEX IF NOT EXISTS appnameidx ON aciinfo (appname)",
   346  	}
   347  
   348  	fn := func(tx *sql.Tx) error {
   349  		for _, stmt := range dbCreateStmts {
   350  			_, err := tx.Exec(stmt)
   351  			if err != nil {
   352  				return err
   353  			}
   354  		}
   355  		return nil
   356  	}
   357  	if err := db.Do(fn); err != nil {
   358  		return err
   359  	}
   360  
   361  	fn = func(tx *sql.Tx) error {
   362  		for _, aciinfo := range aciInfos {
   363  			_, err := tx.Exec("INSERT into aciinfo values ($1, $2, $3, $4)", aciinfo.BlobKey, aciinfo.AppName, aciinfo.ImportTime, aciinfo.Latest)
   364  			if err != nil {
   365  				return err
   366  			}
   367  		}
   368  		return nil
   369  	}
   370  	if err := db.Do(fn); err != nil {
   371  		return err
   372  	}
   373  
   374  	fn = func(tx *sql.Tx) error {
   375  		for _, remote := range remotes {
   376  			_, err := tx.Exec("INSERT into remote values ($1, $2, $3, $4)", remote.ACIURL, remote.SigURL, remote.ETag, remote.BlobKey)
   377  			if err != nil {
   378  				return err
   379  			}
   380  		}
   381  		return nil
   382  	}
   383  	if err := db.Do(fn); err != nil {
   384  		return err
   385  	}
   386  
   387  	return nil
   388  }
   389  
   390  func populateDBV2(db *DB, dbVersion int, aciInfos []*ACIInfoV0_2, remotes []*RemoteV2_3) error {
   391  	var dbCreateStmts = [...]string{
   392  		// version table
   393  		"CREATE TABLE IF NOT EXISTS version (version int);",
   394  		fmt.Sprintf("INSERT INTO version VALUES (%d)", dbVersion),
   395  
   396  		// remote table. The primary key is "aciurl".
   397  		"CREATE TABLE IF NOT EXISTS remote (aciurl string, sigurl string, etag string, blobkey string, cachemaxage int, downloadtime time);",
   398  		"CREATE UNIQUE INDEX IF NOT EXISTS aciurlidx ON remote (aciurl)",
   399  
   400  		// aciinfo table. The primary key is "blobkey" and it matches the key used to save that aci in the blob store
   401  		"CREATE TABLE IF NOT EXISTS aciinfo (blobkey string, appname string, importtime time, latest bool);",
   402  		"CREATE UNIQUE INDEX IF NOT EXISTS blobkeyidx ON aciinfo (blobkey)",
   403  		"CREATE INDEX IF NOT EXISTS appnameidx ON aciinfo (appname)",
   404  	}
   405  
   406  	fn := func(tx *sql.Tx) error {
   407  		for _, stmt := range dbCreateStmts {
   408  			_, err := tx.Exec(stmt)
   409  			if err != nil {
   410  				return err
   411  			}
   412  		}
   413  		return nil
   414  	}
   415  	if err := db.Do(fn); err != nil {
   416  		return err
   417  	}
   418  
   419  	fn = func(tx *sql.Tx) error {
   420  		for _, aciinfo := range aciInfos {
   421  			_, err := tx.Exec("INSERT into aciinfo values ($1, $2, $3, $4)", aciinfo.BlobKey, aciinfo.AppName, aciinfo.ImportTime, aciinfo.Latest)
   422  			if err != nil {
   423  				return err
   424  			}
   425  		}
   426  		return nil
   427  	}
   428  	if err := db.Do(fn); err != nil {
   429  		return err
   430  	}
   431  
   432  	fn = func(tx *sql.Tx) error {
   433  		for _, remote := range remotes {
   434  			_, err := tx.Exec("INSERT into remote values ($1, $2, $3, $4, $5, $6)", remote.ACIURL, remote.SigURL, remote.ETag, remote.BlobKey, remote.CacheMaxAge, remote.DownloadTime)
   435  			if err != nil {
   436  				return err
   437  			}
   438  		}
   439  		return nil
   440  	}
   441  	if err := db.Do(fn); err != nil {
   442  		return err
   443  	}
   444  
   445  	return nil
   446  }
   447  
   448  func populateDBV3(db *DB, dbVersion int, aciInfos []*ACIInfoV3, remotes []*RemoteV2_3) error {
   449  	var dbCreateStmts = [...]string{
   450  		// version table
   451  		"CREATE TABLE IF NOT EXISTS version (version int);",
   452  		fmt.Sprintf("INSERT INTO version VALUES (%d)", dbVersion),
   453  
   454  		// remote table. The primary key is "aciurl".
   455  		"CREATE TABLE IF NOT EXISTS remote (aciurl string, sigurl string, etag string, blobkey string, cachemaxage int, downloadtime time);",
   456  		"CREATE UNIQUE INDEX IF NOT EXISTS aciurlidx ON remote (aciurl)",
   457  
   458  		// aciinfo table. The primary key is "blobkey" and it matches the key used to save that aci in the blob store
   459  		"CREATE TABLE IF NOT EXISTS aciinfo (blobkey string, importtime time, latest bool, name string);",
   460  		"CREATE UNIQUE INDEX IF NOT EXISTS blobkeyidx ON aciinfo (blobkey)",
   461  		"CREATE INDEX IF NOT EXISTS nameidx ON aciinfo (name)",
   462  	}
   463  	fn := func(tx *sql.Tx) error {
   464  		for _, stmt := range dbCreateStmts {
   465  			_, err := tx.Exec(stmt)
   466  			if err != nil {
   467  				return err
   468  			}
   469  		}
   470  		return nil
   471  	}
   472  	if err := db.Do(fn); err != nil {
   473  		return err
   474  	}
   475  
   476  	fn = func(tx *sql.Tx) error {
   477  		for _, aciinfo := range aciInfos {
   478  			_, err := tx.Exec("INSERT into aciinfo values ($1, $2, $3, $4)", aciinfo.BlobKey, aciinfo.ImportTime, aciinfo.Latest, aciinfo.Name)
   479  			if err != nil {
   480  				return err
   481  			}
   482  		}
   483  		return nil
   484  	}
   485  	if err := db.Do(fn); err != nil {
   486  		return err
   487  	}
   488  
   489  	fn = func(tx *sql.Tx) error {
   490  		for _, remote := range remotes {
   491  			_, err := tx.Exec("INSERT into remote values ($1, $2, $3, $4, $5, $6)", remote.ACIURL, remote.SigURL, remote.ETag, remote.BlobKey, remote.CacheMaxAge, remote.DownloadTime)
   492  			if err != nil {
   493  				return err
   494  			}
   495  		}
   496  		return nil
   497  	}
   498  	if err := db.Do(fn); err != nil {
   499  		return err
   500  	}
   501  
   502  	return nil
   503  
   504  }
   505  
   506  type migrateTest struct {
   507  	predb  testdb
   508  	postdb testdb
   509  	// Needed to have the right DB type to load from
   510  	curdb testdb
   511  }
   512  
   513  func testMigrate(tt migrateTest) error {
   514  	dir, err := ioutil.TempDir("", tstprefix)
   515  	if err != nil {
   516  		return fmt.Errorf("error creating tempdir: %v", err)
   517  	}
   518  	defer os.RemoveAll(dir)
   519  
   520  	casDir := filepath.Join(dir, "cas")
   521  	db, err := NewDB(filepath.Join(casDir, "db"))
   522  	if err != nil {
   523  		return err
   524  	}
   525  	if err = tt.predb.populate(db); err != nil {
   526  		return err
   527  	}
   528  
   529  	fn := func(tx *sql.Tx) error {
   530  		err := migrate(tx, tt.postdb.version())
   531  		if err != nil {
   532  			return err
   533  		}
   534  		return nil
   535  	}
   536  	if err = db.Do(fn); err != nil {
   537  		return err
   538  	}
   539  
   540  	var curDBVersion int
   541  	fn = func(tx *sql.Tx) error {
   542  		var err error
   543  		curDBVersion, err = getDBVersion(tx)
   544  		if err != nil {
   545  			return err
   546  		}
   547  		return nil
   548  	}
   549  	if err = db.Do(fn); err != nil {
   550  		return err
   551  	}
   552  	if curDBVersion != tt.postdb.version() {
   553  		return fmt.Errorf("wrong db version: got %#v, want %#v", curDBVersion, tt.postdb.version())
   554  	}
   555  
   556  	if err := tt.curdb.load(db); err != nil {
   557  		return err
   558  	}
   559  	if !tt.curdb.compare(tt.postdb) {
   560  		// TODO(sgotti) not very useful as these are pointers.
   561  		// Use something like go-spew to write the full data?
   562  		return fmt.Errorf("got %#v, want %#v", tt.curdb, tt.postdb)
   563  	}
   564  	return nil
   565  }
   566  
   567  func TestMigrate(t *testing.T) {
   568  	dir, err := ioutil.TempDir("", tstprefix)
   569  	if err != nil {
   570  		t.Fatalf("error creating tempdir: %v", err)
   571  	}
   572  	defer os.RemoveAll(dir)
   573  
   574  	now := time.Now()
   575  	tests := []migrateTest{
   576  		// Test migration from V0 to V1
   577  		// Empty db
   578  		{
   579  			&DBV0{},
   580  			&DBV1{},
   581  			&DBV1{},
   582  		},
   583  		{
   584  			&DBV0{
   585  				[]*ACIInfoV0_2{
   586  					{"sha512-aaaaaaaa", "example.com/app01", now, false},
   587  					{"sha512-bbbbbbbb", "example.com/app02", now, true},
   588  				},
   589  				[]*RemoteV0_1{
   590  					{"http://example.com/app01.aci", "http://example.com/app01.aci.asc", "", "sha512-aaaaaaaa"},
   591  					{"http://example.com/app02.aci", "http://example.com/app02.aci.asc", "", "sha512-bbbbbbbb"},
   592  				},
   593  			},
   594  			&DBV1{
   595  				[]*ACIInfoV0_2{
   596  					{"sha512-aaaaaaaa", "example.com/app01", now, false},
   597  					{"sha512-bbbbbbbb", "example.com/app02", now, true},
   598  				},
   599  				[]*RemoteV0_1{
   600  					{"http://example.com/app01.aci", "http://example.com/app01.aci.asc", "", "sha512-aaaaaaaa"},
   601  					{"http://example.com/app02.aci", "http://example.com/app02.aci.asc", "", "sha512-bbbbbbbb"},
   602  				},
   603  			},
   604  			&DBV1{},
   605  		},
   606  		// Test migration from V1 to V2
   607  		{
   608  			&DBV1{
   609  				[]*ACIInfoV0_2{
   610  					{"sha512-aaaaaaaa", "example.com/app01", now, false},
   611  					{"sha512-bbbbbbbb", "example.com/app02", now, true},
   612  				},
   613  				[]*RemoteV0_1{
   614  					{"http://example.com/app01.aci", "http://example.com/app01.aci.asc", "", "sha512-aaaaaaaa"},
   615  					{"http://example.com/app02.aci", "http://example.com/app02.aci.asc", "", "sha512-bbbbbbbb"},
   616  				},
   617  			},
   618  			&DBV2{
   619  				[]*ACIInfoV0_2{
   620  					{"sha512-aaaaaaaa", "example.com/app01", now, false},
   621  					{"sha512-bbbbbbbb", "example.com/app02", now, true},
   622  				},
   623  				[]*RemoteV2_3{
   624  					{"http://example.com/app01.aci", "http://example.com/app01.aci.asc", "", "sha512-aaaaaaaa", 0, time.Time{}.UTC()},
   625  					{"http://example.com/app02.aci", "http://example.com/app02.aci.asc", "", "sha512-bbbbbbbb", 0, time.Time{}.UTC()},
   626  				},
   627  			},
   628  			&DBV2{},
   629  		},
   630  		// Test migration from V1 to V3
   631  		{
   632  			&DBV2{
   633  				[]*ACIInfoV0_2{
   634  					{"sha512-aaaaaaaa", "example.com/app01", now, false},
   635  					{"sha512-bbbbbbbb", "example.com/app02", now, true},
   636  				},
   637  				[]*RemoteV2_3{
   638  					{"http://example.com/app01.aci", "http://example.com/app01.aci.asc", "", "sha512-aaaaaaaa", 0, time.Time{}.UTC()},
   639  					{"http://example.com/app02.aci", "http://example.com/app02.aci.asc", "", "sha512-bbbbbbbb", 0, time.Time{}.UTC()},
   640  				},
   641  			},
   642  			&DBV3{
   643  				[]*ACIInfoV3{
   644  					{"sha512-aaaaaaaa", "example.com/app01", now, false},
   645  					{"sha512-bbbbbbbb", "example.com/app02", now, true},
   646  				},
   647  				[]*RemoteV2_3{
   648  					{"http://example.com/app01.aci", "http://example.com/app01.aci.asc", "", "sha512-aaaaaaaa", 0, time.Time{}.UTC()},
   649  					{"http://example.com/app02.aci", "http://example.com/app02.aci.asc", "", "sha512-bbbbbbbb", 0, time.Time{}.UTC()},
   650  				},
   651  			},
   652  			&DBV3{},
   653  		},
   654  	}
   655  
   656  	for i, tt := range tests {
   657  		if err := testMigrate(tt); err != nil {
   658  			t.Errorf("#%d: unexpected error: %v", i, err)
   659  		}
   660  	}
   661  }
   662  
   663  // compareSlices compare slices regardless of the slice elements order
   664  func compareSlicesNoOrder(i1 interface{}, i2 interface{}) bool {
   665  	s1 := interfaceToSlice(i1)
   666  	s2 := interfaceToSlice(i2)
   667  
   668  	if len(s1) != len(s2) {
   669  		return false
   670  	}
   671  
   672  	seen := map[int]bool{}
   673  	for _, v1 := range s1 {
   674  		found := false
   675  		for i2, v2 := range s2 {
   676  			if _, ok := seen[i2]; ok {
   677  				continue
   678  			}
   679  			if reflect.DeepEqual(v1, v2) {
   680  				found = true
   681  				seen[i2] = true
   682  				continue
   683  			}
   684  		}
   685  		if !found {
   686  			return false
   687  		}
   688  
   689  	}
   690  	return true
   691  }
   692  
   693  func interfaceToSlice(s interface{}) []interface{} {
   694  	v := reflect.ValueOf(s)
   695  	if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
   696  		panic(fmt.Errorf("Expected slice or array, got %T", s))
   697  	}
   698  	l := v.Len()
   699  	m := make([]interface{}, l)
   700  	for i := 0; i < l; i++ {
   701  		m[i] = v.Index(i).Interface()
   702  	}
   703  	return m
   704  }