2016-12-28 18:03:40 -07:00
|
|
|
package testfixtures
|
|
|
|
|
|
|
|
import (
|
|
|
|
"database/sql"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"path"
|
|
|
|
"path/filepath"
|
|
|
|
"regexp"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"gopkg.in/yaml.v2"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Context holds the fixtures to be loaded in the database.
|
|
|
|
type Context struct {
|
|
|
|
db *sql.DB
|
|
|
|
helper Helper
|
|
|
|
fixturesFiles []*fixtureFile
|
|
|
|
}
|
|
|
|
|
|
|
|
type fixtureFile struct {
|
|
|
|
path string
|
|
|
|
fileName string
|
|
|
|
content []byte
|
|
|
|
insertSQLs []insertSQL
|
|
|
|
}
|
|
|
|
|
|
|
|
type insertSQL struct {
|
|
|
|
sql string
|
|
|
|
params []interface{}
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
dbnameRegexp = regexp.MustCompile("(?i)test")
|
|
|
|
)
|
|
|
|
|
2018-10-02 13:20:02 -06:00
|
|
|
// NewFolder creates a context for all fixtures in a given folder into the database:
|
2016-12-28 18:03:40 -07:00
|
|
|
// NewFolder(db, &PostgreSQL{}, "my/fixtures/folder")
|
|
|
|
func NewFolder(db *sql.DB, helper Helper, folderName string) (*Context, error) {
|
|
|
|
fixtures, err := fixturesFromFolder(folderName)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
c, err := newContext(db, helper, fixtures)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return c, nil
|
|
|
|
}
|
|
|
|
|
2018-10-02 13:20:02 -06:00
|
|
|
// NewFiles creates a context for all specified fixtures files into database:
|
2016-12-28 18:03:40 -07:00
|
|
|
// NewFiles(db, &PostgreSQL{},
|
|
|
|
// "fixtures/customers.yml",
|
|
|
|
// "fixtures/orders.yml"
|
|
|
|
// // add as many files you want
|
|
|
|
// )
|
|
|
|
func NewFiles(db *sql.DB, helper Helper, fileNames ...string) (*Context, error) {
|
|
|
|
fixtures, err := fixturesFromFiles(fileNames...)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
c, err := newContext(db, helper, fixtures)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return c, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func newContext(db *sql.DB, helper Helper, fixtures []*fixtureFile) (*Context, error) {
|
|
|
|
c := &Context{
|
|
|
|
db: db,
|
|
|
|
helper: helper,
|
|
|
|
fixturesFiles: fixtures,
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := c.helper.init(c.db); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := c.buildInsertSQLs(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return c, nil
|
|
|
|
}
|
|
|
|
|
2018-10-02 13:20:02 -06:00
|
|
|
// DetectTestDatabase returns nil if databaseName matches regexp
|
|
|
|
// if err := fixtures.DetectTestDatabase(); err != nil {
|
|
|
|
// log.Fatal(err)
|
|
|
|
// }
|
|
|
|
func (c *Context) DetectTestDatabase() error {
|
|
|
|
dbName, err := c.helper.databaseName(c.db)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if !dbnameRegexp.MatchString(dbName) {
|
|
|
|
return ErrNotTestDatabase
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-12-28 18:03:40 -07:00
|
|
|
// Load wipes and after load all fixtures in the database.
|
|
|
|
// if err := fixtures.Load(); err != nil {
|
|
|
|
// log.Fatal(err)
|
|
|
|
// }
|
|
|
|
func (c *Context) Load() error {
|
|
|
|
if !skipDatabaseNameCheck {
|
2018-10-02 13:20:02 -06:00
|
|
|
if err := c.DetectTestDatabase(); err != nil {
|
|
|
|
return err
|
2016-12-28 18:03:40 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
err := c.helper.disableReferentialIntegrity(c.db, func(tx *sql.Tx) error {
|
|
|
|
for _, file := range c.fixturesFiles {
|
2018-10-02 13:20:02 -06:00
|
|
|
modified, err := c.helper.isTableModified(tx, file.fileNameWithoutExtension())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if !modified {
|
|
|
|
continue
|
|
|
|
}
|
2016-12-28 18:03:40 -07:00
|
|
|
if err := file.delete(tx, c.helper); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-10-02 13:20:02 -06:00
|
|
|
err = c.helper.whileInsertOnTable(tx, file.fileNameWithoutExtension(), func() error {
|
|
|
|
for j, i := range file.insertSQLs {
|
2016-12-28 18:03:40 -07:00
|
|
|
if _, err := tx.Exec(i.sql, i.params...); err != nil {
|
2018-10-02 13:20:02 -06:00
|
|
|
return &InsertError{
|
|
|
|
Err: err,
|
|
|
|
File: file.fileName,
|
|
|
|
Index: j,
|
|
|
|
SQL: i.sql,
|
|
|
|
Params: i.params,
|
|
|
|
}
|
2016-12-28 18:03:40 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
2018-10-02 13:20:02 -06:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return c.helper.afterLoad(c.db)
|
2016-12-28 18:03:40 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Context) buildInsertSQLs() error {
|
|
|
|
for _, f := range c.fixturesFiles {
|
|
|
|
var records interface{}
|
|
|
|
if err := yaml.Unmarshal(f.content, &records); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
switch records := records.(type) {
|
|
|
|
case []interface{}:
|
|
|
|
for _, record := range records {
|
|
|
|
recordMap, ok := record.(map[interface{}]interface{})
|
|
|
|
if !ok {
|
|
|
|
return ErrWrongCastNotAMap
|
|
|
|
}
|
|
|
|
|
|
|
|
sql, values, err := f.buildInsertSQL(c.helper, recordMap)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
f.insertSQLs = append(f.insertSQLs, insertSQL{sql, values})
|
|
|
|
}
|
|
|
|
case map[interface{}]interface{}:
|
|
|
|
for _, record := range records {
|
|
|
|
recordMap, ok := record.(map[interface{}]interface{})
|
|
|
|
if !ok {
|
|
|
|
return ErrWrongCastNotAMap
|
|
|
|
}
|
|
|
|
|
|
|
|
sql, values, err := f.buildInsertSQL(c.helper, recordMap)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
f.insertSQLs = append(f.insertSQLs, insertSQL{sql, values})
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
return ErrFileIsNotSliceOrMap
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *fixtureFile) fileNameWithoutExtension() string {
|
|
|
|
return strings.Replace(f.fileName, filepath.Ext(f.fileName), "", 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *fixtureFile) delete(tx *sql.Tx, h Helper) error {
|
|
|
|
_, err := tx.Exec(fmt.Sprintf("DELETE FROM %s", h.quoteKeyword(f.fileNameWithoutExtension())))
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *fixtureFile) buildInsertSQL(h Helper, record map[interface{}]interface{}) (sqlStr string, values []interface{}, err error) {
|
|
|
|
var (
|
|
|
|
sqlColumns []string
|
|
|
|
sqlValues []string
|
|
|
|
i = 1
|
|
|
|
)
|
|
|
|
for key, value := range record {
|
|
|
|
keyStr, ok := key.(string)
|
|
|
|
if !ok {
|
|
|
|
err = ErrKeyIsNotString
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
sqlColumns = append(sqlColumns, h.quoteKeyword(keyStr))
|
|
|
|
|
2018-10-02 13:20:02 -06:00
|
|
|
// if string, try convert to SQL or time
|
|
|
|
// if map or array, convert to json
|
|
|
|
switch v := value.(type) {
|
|
|
|
case string:
|
|
|
|
if strings.HasPrefix(v, "RAW=") {
|
|
|
|
sqlValues = append(sqlValues, strings.TrimPrefix(v, "RAW="))
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if t, err := tryStrToDate(v); err == nil {
|
|
|
|
value = t
|
|
|
|
}
|
|
|
|
case []interface{}, map[interface{}]interface{}:
|
|
|
|
value = recursiveToJSON(v)
|
|
|
|
}
|
|
|
|
|
2016-12-28 18:03:40 -07:00
|
|
|
switch h.paramType() {
|
|
|
|
case paramTypeDollar:
|
|
|
|
sqlValues = append(sqlValues, fmt.Sprintf("$%d", i))
|
|
|
|
case paramTypeQuestion:
|
|
|
|
sqlValues = append(sqlValues, "?")
|
|
|
|
case paramTypeColon:
|
2018-10-02 13:20:02 -06:00
|
|
|
sqlValues = append(sqlValues, fmt.Sprintf(":%d", i))
|
2016-12-28 18:03:40 -07:00
|
|
|
}
|
2018-10-02 13:20:02 -06:00
|
|
|
|
2016-12-28 18:03:40 -07:00
|
|
|
values = append(values, value)
|
2018-10-02 13:20:02 -06:00
|
|
|
i++
|
2016-12-28 18:03:40 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
sqlStr = fmt.Sprintf(
|
|
|
|
"INSERT INTO %s (%s) VALUES (%s)",
|
|
|
|
h.quoteKeyword(f.fileNameWithoutExtension()),
|
|
|
|
strings.Join(sqlColumns, ", "),
|
|
|
|
strings.Join(sqlValues, ", "),
|
|
|
|
)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func fixturesFromFolder(folderName string) ([]*fixtureFile, error) {
|
|
|
|
var files []*fixtureFile
|
|
|
|
fileinfos, err := ioutil.ReadDir(folderName)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, fileinfo := range fileinfos {
|
|
|
|
if !fileinfo.IsDir() && filepath.Ext(fileinfo.Name()) == ".yml" {
|
|
|
|
fixture := &fixtureFile{
|
|
|
|
path: path.Join(folderName, fileinfo.Name()),
|
|
|
|
fileName: fileinfo.Name(),
|
|
|
|
}
|
|
|
|
fixture.content, err = ioutil.ReadFile(fixture.path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
files = append(files, fixture)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return files, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func fixturesFromFiles(fileNames ...string) ([]*fixtureFile, error) {
|
|
|
|
var (
|
|
|
|
fixtureFiles []*fixtureFile
|
|
|
|
err error
|
|
|
|
)
|
|
|
|
|
|
|
|
for _, f := range fileNames {
|
|
|
|
fixture := &fixtureFile{
|
|
|
|
path: f,
|
|
|
|
fileName: filepath.Base(f),
|
|
|
|
}
|
|
|
|
fixture.content, err = ioutil.ReadFile(fixture.path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
fixtureFiles = append(fixtureFiles, fixture)
|
|
|
|
}
|
|
|
|
|
|
|
|
return fixtureFiles, nil
|
|
|
|
}
|