upgrade to use testfixtures v3 (#11904)
* upgrade to use testfixtures v3 * simplify logic * make vendor * update per @lunny * Update templates/repo/empty.tmpl * Update templates/repo/empty.tmpl Co-authored-by: Lauris BH <lauris@nix.lv>tokarchuk/v1.17
parent
1645d4a5d8
commit
9e6a79bea9
@ -0,0 +1,41 @@ |
|||||||
|
build: |
||||||
|
binary: testfixtures |
||||||
|
main: ./cmd/testfixtures |
||||||
|
goos: |
||||||
|
- windows |
||||||
|
- darwin |
||||||
|
- linux |
||||||
|
goarch: |
||||||
|
- 386 |
||||||
|
- amd64 |
||||||
|
ignore: |
||||||
|
- goos: darwin |
||||||
|
goarch: 386 |
||||||
|
flags: |
||||||
|
- -tags=sqlite |
||||||
|
|
||||||
|
archives: |
||||||
|
- name_template: "{{.Binary}}_{{.Os}}_{{.Arch}}" |
||||||
|
format_overrides: |
||||||
|
- goos: windows |
||||||
|
format: zip |
||||||
|
|
||||||
|
release: |
||||||
|
draft: true |
||||||
|
|
||||||
|
snapshot: |
||||||
|
name_template: "{{.Tag}}" |
||||||
|
|
||||||
|
checksum: |
||||||
|
name_template: "testfixtures_checksums.txt" |
||||||
|
|
||||||
|
nfpms: |
||||||
|
- vendor: testfixtures |
||||||
|
homepage: https://github.com/go-testfixtures/testfixtures |
||||||
|
maintainer: Andrey Nering <andrey.nering@gmail.com> |
||||||
|
description: Ruby on Rails like test fixtures for Go. |
||||||
|
license: MIT |
||||||
|
formats: |
||||||
|
- deb |
||||||
|
- rpm |
||||||
|
file_name_template: "{{.ProjectName}}_{{.Os}}_{{.Arch}}" |
@ -0,0 +1,93 @@ |
|||||||
|
# Changelog |
||||||
|
|
||||||
|
## v3.2.0 - 2020-05-10 |
||||||
|
|
||||||
|
- Add support for loading multiple files and directories |
||||||
|
([#65](https://github.com/go-testfixtures/testfixtures/pull/65)). |
||||||
|
|
||||||
|
## v3.1.2 - 2020-04-26 |
||||||
|
|
||||||
|
- Dump: Fix column order in generated YAML files |
||||||
|
([#62](https://github.com/go-testfixtures/testfixtures/pull/62)). |
||||||
|
|
||||||
|
## v3.1.1 - 2020-01-11 |
||||||
|
|
||||||
|
- testfixtures now work with both `mssql` and `sqlserver` drivers. |
||||||
|
Note that [the `mssql` one is deprecated](https://github.com/denisenkom/go-mssqldb#deprecated), |
||||||
|
though. So try to migrate to `sqlserver` once possible. |
||||||
|
|
||||||
|
## v3.1.0 - 2020-01-09 |
||||||
|
|
||||||
|
- Using `sqlserver` driver instead of the deprecated `mssql` |
||||||
|
([#58](https://github.com/go-testfixtures/testfixtures/pull/58)). |
||||||
|
|
||||||
|
## v3.0.0 - 2019-12-26 |
||||||
|
|
||||||
|
### Breaking changes |
||||||
|
|
||||||
|
- The import path changed from `gopkg.in/testfixtures.v2` to |
||||||
|
`github.com/go-testfixtures/testfixtures/v3`. |
||||||
|
- This package no longer support Oracle databases. This decision was |
||||||
|
taken because too few people actually used this package with Oracle and it |
||||||
|
was the most difficult to test (we didn't run on CI due the lack of an |
||||||
|
official Docker image, etc). |
||||||
|
- The public API was totally rewritten to be more flexible and ideomatic. |
||||||
|
It now uses functional options. It differs from v2, but should be easy |
||||||
|
enough to upgrade. |
||||||
|
- Some deprecated APIs from v2 were removed as well. |
||||||
|
- This now requires Go >= 1.13. |
||||||
|
|
||||||
|
### New features |
||||||
|
|
||||||
|
- We now have a CLI so you can easily use testfixtures to load a sample |
||||||
|
database from fixtures if you want. |
||||||
|
- Templating via [text/template](https://golang.org/pkg/text/template/) |
||||||
|
is now available. This allows some fancier use cases like generating data |
||||||
|
or specific columns dynamically. |
||||||
|
- It's now possible to choose which time zone to use when parsing timestamps |
||||||
|
from fixtures. The default is the same as before, whatever is set on |
||||||
|
`time.Local`. |
||||||
|
- Errors now use the new `%w` verb only available on Go >= 1.13. |
||||||
|
|
||||||
|
### MISC |
||||||
|
|
||||||
|
- Travis and AppVeyor are gone. We're using GitHub Actions exclusively now. |
||||||
|
The whole suite is ran inside Docker (with help of Docker Compose), so it's |
||||||
|
easy to run tests locally as well. |
||||||
|
|
||||||
|
Check the new README for some examples! |
||||||
|
|
||||||
|
## v2.6.0 - 2019-10-24 |
||||||
|
|
||||||
|
- Add support for TimescaleDB |
||||||
|
([#53](https://github.com/go-testfixtures/testfixtures/pull/53)). |
||||||
|
|
||||||
|
## v2.5.3 - 2018-12-15 |
||||||
|
|
||||||
|
- Fixes related to use of foreign key pragmas on MySQL (#43). |
||||||
|
|
||||||
|
## v2.5.2 - 2018-11-25 |
||||||
|
|
||||||
|
- This library now supports [Go Modules](https://github.com/golang/go/wiki/Modules); |
||||||
|
- Also allow `.yaml` (as an alternative to `.yml`) as the file extension (#42). |
||||||
|
|
||||||
|
## v2.5.1 - 2018-11-04 |
||||||
|
|
||||||
|
- Allowing disabling reset of PostgreSQL sequences (#38). |
||||||
|
|
||||||
|
## v2.5.0 - 2018-09-07 |
||||||
|
|
||||||
|
- Add public function DetectTestDatabase (#35, #36). |
||||||
|
|
||||||
|
## v2.4.5 - 2018-07-07 |
||||||
|
|
||||||
|
- Fix for MySQL/MariaDB: ignoring views on operations that should be run only on tables (#33). |
||||||
|
|
||||||
|
## v2.4.4 - 2018-07-02 |
||||||
|
|
||||||
|
- Fix for multiple schemas on Microsoft SQL Server (#29 and #30); |
||||||
|
- Configuring AppVeyor CI to also test for Microsoft SQL Server. |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
Sorry, we don't have changelog for older releases 😢. |
@ -0,0 +1,9 @@ |
|||||||
|
FROM golang:1.14-alpine |
||||||
|
|
||||||
|
RUN apk update |
||||||
|
RUN apk add alpine-sdk |
||||||
|
|
||||||
|
WORKDIR /testfixtures |
||||||
|
COPY . . |
||||||
|
|
||||||
|
RUN go mod download |
@ -0,0 +1,483 @@ |
|||||||
|
# testfixtures |
||||||
|
|
||||||
|
[![GoDoc](https://godoc.org/github.com/go-testfixtures/testfixtures?status.svg)][doc] |
||||||
|
|
||||||
|
> ***Warning***: this package will wipe the database data before loading the |
||||||
|
fixtures! It is supposed to be used on a test database. Please, double check |
||||||
|
if you are running it against the correct database. |
||||||
|
|
||||||
|
> **TIP**: There are options not described in this README page. It's |
||||||
|
> recommended that you also check [the documentation][doc]. |
||||||
|
|
||||||
|
Writing tests is hard, even more when you have to deal with an SQL database. |
||||||
|
This package aims to make writing functional tests for web apps written in |
||||||
|
Go easier. |
||||||
|
|
||||||
|
Basically this package mimics the ["Ruby on Rails' way"][railstests] of writing tests |
||||||
|
for database applications, where sample data is kept in fixtures files. Before |
||||||
|
the execution of every test, the test database is cleaned and the fixture data |
||||||
|
is loaded into the database. |
||||||
|
|
||||||
|
The idea is running tests against a real database, instead of relying in mocks, |
||||||
|
which is boring to setup and may lead to production bugs not being caught in |
||||||
|
the tests. |
||||||
|
|
||||||
|
## Installation |
||||||
|
|
||||||
|
First, import it like this: |
||||||
|
|
||||||
|
```go |
||||||
|
import ( |
||||||
|
"github.com/go-testfixtures/testfixtures/v3" |
||||||
|
) |
||||||
|
``` |
||||||
|
|
||||||
|
## Usage |
||||||
|
|
||||||
|
Create a folder for the fixture files. Each file should contain data for a |
||||||
|
single table and have the name `<table_name>.yml`: |
||||||
|
|
||||||
|
``` |
||||||
|
myapp/ |
||||||
|
myapp.go |
||||||
|
myapp_test.go |
||||||
|
... |
||||||
|
fixtures/ |
||||||
|
posts.yml |
||||||
|
comments.yml |
||||||
|
tags.yml |
||||||
|
posts_tags.yml |
||||||
|
... |
||||||
|
``` |
||||||
|
|
||||||
|
The file would look like this (it can have as many record you want): |
||||||
|
|
||||||
|
```yml |
||||||
|
# comments.yml |
||||||
|
- id: 1 |
||||||
|
post_id: 1 |
||||||
|
content: A comment... |
||||||
|
author_name: John Doe |
||||||
|
author_email: john@doe.com |
||||||
|
created_at: 2020-12-31 23:59:59 |
||||||
|
updated_at: 2020-12-31 23:59:59 |
||||||
|
|
||||||
|
- id: 2 |
||||||
|
post_id: 2 |
||||||
|
content: Another comment... |
||||||
|
author_name: John Doe |
||||||
|
author_email: john@doe.com |
||||||
|
created_at: 2020-12-31 23:59:59 |
||||||
|
updated_at: 2020-12-31 23:59:59 |
||||||
|
|
||||||
|
# ... |
||||||
|
``` |
||||||
|
|
||||||
|
An YAML object or array will be converted to JSON. It will be stored on a native |
||||||
|
JSON type like JSONB on PostgreSQL or as a TEXT or VARCHAR column on other |
||||||
|
databases. |
||||||
|
|
||||||
|
```yml |
||||||
|
- id: 1 |
||||||
|
post_attributes: |
||||||
|
author: John Due |
||||||
|
author_email: john@due.com |
||||||
|
title: "..." |
||||||
|
tags: |
||||||
|
- programming |
||||||
|
- go |
||||||
|
- testing |
||||||
|
post: "..." |
||||||
|
``` |
||||||
|
|
||||||
|
If you need to write raw SQL, probably to call a function, prefix the value |
||||||
|
of the column with `RAW=`: |
||||||
|
|
||||||
|
```yml |
||||||
|
- id: 1 |
||||||
|
uuid_column: RAW=uuid_generate_v4() |
||||||
|
postgis_type_column: RAW=ST_GeomFromText('params...') |
||||||
|
created_at: RAW=NOW() |
||||||
|
updated_at: RAW=NOW() |
||||||
|
``` |
||||||
|
|
||||||
|
Your tests would look like this: |
||||||
|
|
||||||
|
```go |
||||||
|
package myapp |
||||||
|
|
||||||
|
import ( |
||||||
|
"database/sql" |
||||||
|
|
||||||
|
_ "github.com/lib/pq" |
||||||
|
"github.com/go-testfixtures/testfixtures/v3" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
db *sql.DB |
||||||
|
fixtures *testfixtures.Loader |
||||||
|
) |
||||||
|
|
||||||
|
func TestMain(m *testing.M) { |
||||||
|
var err error |
||||||
|
|
||||||
|
// Open connection to the test database. |
||||||
|
// Do NOT import fixtures in a production database! |
||||||
|
// Existing data would be deleted. |
||||||
|
db, err = sql.Open("postgres", "dbname=myapp_test") |
||||||
|
if err != nil { |
||||||
|
... |
||||||
|
} |
||||||
|
|
||||||
|
fixtures, err := testfixtures.New( |
||||||
|
testfixtures.Database(db), // You database connection |
||||||
|
testfixtures.Dialect("postgres"), // Available: "postgresql", "timescaledb", "mysql", "mariadb", "sqlite" and "sqlserver" |
||||||
|
testfixtures.Directory("testdata/fixtures"), // the directory containing the YAML files |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
... |
||||||
|
} |
||||||
|
|
||||||
|
os.Exit(m.Run()) |
||||||
|
} |
||||||
|
|
||||||
|
func prepareTestDatabase() { |
||||||
|
if err := fixtures.Load(); err != nil { |
||||||
|
... |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestX(t *testing.T) { |
||||||
|
prepareTestDatabase() |
||||||
|
|
||||||
|
// Your test here ... |
||||||
|
} |
||||||
|
|
||||||
|
func TestY(t *testing.T) { |
||||||
|
prepareTestDatabase() |
||||||
|
|
||||||
|
// Your test here ... |
||||||
|
} |
||||||
|
|
||||||
|
func TestZ(t *testing.T) { |
||||||
|
prepareTestDatabase() |
||||||
|
|
||||||
|
// Your test here ... |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Alternatively, you can use the `Files` option, to specify which |
||||||
|
files you want to load into the database: |
||||||
|
|
||||||
|
```go |
||||||
|
fixtures, err := testfixtures.New( |
||||||
|
testfixtures.Database(db), |
||||||
|
testfixtures.Dialect("postgres"), |
||||||
|
testfixtures.Files( |
||||||
|
"fixtures/orders.yml", |
||||||
|
"fixtures/customers.yml", |
||||||
|
), |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
... |
||||||
|
} |
||||||
|
|
||||||
|
fixtures, err := testfixtures.NewFiles(db, &testfixtures.PostgreSQL{}, |
||||||
|
"fixtures/orders.yml", |
||||||
|
"fixtures/customers.yml", |
||||||
|
// add as many files you want |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
... |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
With `Paths` option, you can specify the paths that fixtures will load |
||||||
|
from. Path can be directory or file. If directory, we will search YAML files |
||||||
|
in it. |
||||||
|
|
||||||
|
```go |
||||||
|
fixtures, err := testfixtures.New( |
||||||
|
testfixtures.Database(db), |
||||||
|
testfixtures.Dialect("postgres"), |
||||||
|
testfixtures.Paths( |
||||||
|
"fixtures/orders.yml", |
||||||
|
"fixtures/customers.yml", |
||||||
|
"common_fixtures/users" |
||||||
|
), |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
... |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## Security check |
||||||
|
|
||||||
|
In order to prevent you from accidentally wiping the wrong database, this |
||||||
|
package will refuse to load fixtures if the database name (or database |
||||||
|
filename for SQLite) doesn't contains "test". If you want to disable this |
||||||
|
check, use: |
||||||
|
|
||||||
|
```go |
||||||
|
testfixtures.New( |
||||||
|
... |
||||||
|
testfixtures.DangerousSkipTestDatabaseCheck(), |
||||||
|
) |
||||||
|
``` |
||||||
|
|
||||||
|
## Sequences |
||||||
|
|
||||||
|
For PostgreSQL, this package also resets all sequences to a high |
||||||
|
number to prevent duplicated primary keys while running the tests. |
||||||
|
The default is 10000, but you can change that with: |
||||||
|
|
||||||
|
```go |
||||||
|
testfixtures.New( |
||||||
|
... |
||||||
|
testfixtures.ResetSequencesTo(10000), |
||||||
|
) |
||||||
|
``` |
||||||
|
|
||||||
|
Or, if you want to skip the reset of sequences entirely: |
||||||
|
|
||||||
|
```go |
||||||
|
testfixtures.New( |
||||||
|
... |
||||||
|
testfixtures.SkipResetSequences(), |
||||||
|
) |
||||||
|
``` |
||||||
|
|
||||||
|
## Compatible databases |
||||||
|
|
||||||
|
### PostgreSQL / TimescaleDB |
||||||
|
|
||||||
|
This package has two approaches to disable foreign keys while importing fixtures |
||||||
|
for PostgreSQL databases: |
||||||
|
|
||||||
|
#### With `DISABLE TRIGGER` |
||||||
|
|
||||||
|
This is the default approach. For that use: |
||||||
|
|
||||||
|
```go |
||||||
|
testfixtures.New( |
||||||
|
... |
||||||
|
testfixtures.Dialect("postgres"), // or "timescaledb" |
||||||
|
) |
||||||
|
``` |
||||||
|
|
||||||
|
With the above snippet this package will use `DISABLE TRIGGER` to temporarily |
||||||
|
disabling foreign key constraints while loading fixtures. This work with any |
||||||
|
version of PostgreSQL, but it is **required** to be connected in the database |
||||||
|
as a SUPERUSER. You can make a PostgreSQL user a SUPERUSER with: |
||||||
|
|
||||||
|
```sql |
||||||
|
ALTER USER your_user SUPERUSER; |
||||||
|
``` |
||||||
|
|
||||||
|
#### With `ALTER CONSTRAINT` |
||||||
|
|
||||||
|
This approach don't require to be connected as a SUPERUSER, but only work with |
||||||
|
PostgreSQL versions >= 9.4. Try this if you are getting foreign key violation |
||||||
|
errors with the previous approach. It is as simple as using: |
||||||
|
|
||||||
|
```go |
||||||
|
testfixtures.New( |
||||||
|
... |
||||||
|
testfixtures.Dialect("postgres"), |
||||||
|
testfixtures.UseAlterConstraint(), |
||||||
|
) |
||||||
|
``` |
||||||
|
|
||||||
|
Tested using the [github.com/lib/pq](https://github.com/lib/pq) driver. |
||||||
|
|
||||||
|
### MySQL / MariaDB |
||||||
|
|
||||||
|
Just make sure the connection string have |
||||||
|
[the multistatement parameter](https://github.com/go-sql-driver/mysql#multistatements) |
||||||
|
set to true, and use: |
||||||
|
|
||||||
|
```go |
||||||
|
testfixtures.New( |
||||||
|
... |
||||||
|
testfixtures.Dialect("mysql"), // or "mariadb" |
||||||
|
) |
||||||
|
``` |
||||||
|
|
||||||
|
Tested using the [github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql) driver. |
||||||
|
|
||||||
|
### SQLite |
||||||
|
|
||||||
|
SQLite is also supported. It is recommended to create foreign keys as |
||||||
|
`DEFERRABLE` (the default) to prevent problems. See more |
||||||
|
[on the SQLite documentation](https://www.sqlite.org/foreignkeys.html#fk_deferred). |
||||||
|
(Foreign key constraints are no-op by default on SQLite, but enabling it is |
||||||
|
recommended). |
||||||
|
|
||||||
|
```go |
||||||
|
testfixtures.New( |
||||||
|
... |
||||||
|
testfixtures.Dialect("sqlite"), |
||||||
|
) |
||||||
|
``` |
||||||
|
|
||||||
|
Tested using the [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) driver. |
||||||
|
|
||||||
|
### Microsoft SQL Server |
||||||
|
|
||||||
|
SQL Server support requires SQL Server >= 2008. Inserting on `IDENTITY` columns |
||||||
|
are handled as well. Just make sure you are logged in with a user with |
||||||
|
`ALTER TABLE` permission. |
||||||
|
|
||||||
|
```go |
||||||
|
testfixtures.New( |
||||||
|
... |
||||||
|
testfixtures.Dialect("sqlserver"), |
||||||
|
) |
||||||
|
``` |
||||||
|
|
||||||
|
Tested using the `mssql` and `sqlserver` drivers from the |
||||||
|
[github.com/denisenkom/go-mssqldb](https://github.com/denisenkom/go-mssqldb) lib. |
||||||
|
|
||||||
|
## Templating |
||||||
|
|
||||||
|
Testfixtures supports templating, but it's disabled by default. Most people |
||||||
|
won't need it, but it may be useful to dynamically generate data. |
||||||
|
|
||||||
|
Enable it by doing: |
||||||
|
|
||||||
|
```go |
||||||
|
testfixtures.New( |
||||||
|
... |
||||||
|
testfixtures.Template(), |
||||||
|
|
||||||
|
// the above options are optional |
||||||
|
TemplateFuncs(...), |
||||||
|
TemplateDelims("{{", "}}"), |
||||||
|
TemplateOptions("missingkey=zero"), |
||||||
|
TemplateData(...), |
||||||
|
) |
||||||
|
``` |
||||||
|
|
||||||
|
The YAML file could look like this: |
||||||
|
|
||||||
|
```yaml |
||||||
|
# It's possible generate values... |
||||||
|
- id: {{sha256 "my-awesome-post}} |
||||||
|
title: My Awesome Post |
||||||
|
text: {{randomText}} |
||||||
|
|
||||||
|
# ... or records |
||||||
|
{{range $post := $.Posts}} |
||||||
|
- id: {{$post.Id}} |
||||||
|
title: {{$post.Title}} |
||||||
|
text: {{$post.Text}} |
||||||
|
{{end}} |
||||||
|
``` |
||||||
|
|
||||||
|
## Generating fixtures for a existing database |
||||||
|
|
||||||
|
The following code will generate a YAML file for each table of the database |
||||||
|
into a given folder. It may be useful to boostrap a test scenario from a sample |
||||||
|
database of your app. |
||||||
|
|
||||||
|
```go |
||||||
|
dumper, err := testfixtures.NewDumper( |
||||||
|
testfixtures.DumpDatabase(db), |
||||||
|
testfixtures.DumpDialect("postgres"), // or your database of choice |
||||||
|
testfixtures.DumpDirectory("tmp/fixtures"), |
||||||
|
textfixtures.DumpTables( // optional, will dump all table if not given |
||||||
|
"posts", |
||||||
|
"comments", |
||||||
|
"tags", |
||||||
|
) |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
... |
||||||
|
} |
||||||
|
if err := dumper.Dump(); err != nil { |
||||||
|
... |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
> This was intended to run in small sample databases. It will likely break |
||||||
|
if run in a production/big database. |
||||||
|
|
||||||
|
## Gotchas |
||||||
|
|
||||||
|
### Parallel testing |
||||||
|
|
||||||
|
This library doesn't yet support running tests in parallel! Running tests |
||||||
|
in parallel can result in random data being present in the database, which |
||||||
|
will likely cause tests to randomly/intermittently fail. |
||||||
|
|
||||||
|
This is specially tricky since it's not immediately clear that `go test ./...` |
||||||
|
run tests for each package in parallel. If more than one package use this |
||||||
|
library, you can face this issue. Please, use `go test -p 1 ./...` or run tests |
||||||
|
for each package in separated commands to fix this issue. |
||||||
|
|
||||||
|
If you're looking into being able to run tests in parallel you can try using |
||||||
|
testfixtures together with the [txdb][gotxdb] package, which allows wrapping |
||||||
|
each test run in a transaction. |
||||||
|
|
||||||
|
## CLI |
||||||
|
|
||||||
|
We also have a CLI to load fixtures in a given database. |
||||||
|
Grab it from the [releases page](https://github.com/go-testfixtures/testfixtures/releases) |
||||||
|
and use it like: |
||||||
|
|
||||||
|
```bash |
||||||
|
testfixtures -d postgres -c "postgres://user:password@localhost/database" -D testdata/fixtures |
||||||
|
``` |
||||||
|
|
||||||
|
The connection string changes for each database driver. |
||||||
|
|
||||||
|
Use `--help` for all flags. |
||||||
|
|
||||||
|
## Contributing |
||||||
|
|
||||||
|
We recommend you to [install Task](https://taskfile.dev/#/installation) and |
||||||
|
Docker before contributing to this package, since some stuff is automated |
||||||
|
using these tools. |
||||||
|
|
||||||
|
It's recommended to use Docker Compose to run tests, since it runs tests for |
||||||
|
all supported databases once. To do that you just need to run: |
||||||
|
|
||||||
|
```bash |
||||||
|
task docker |
||||||
|
``` |
||||||
|
|
||||||
|
But if you want to run tests locally, copy the `.sample.env` file as `.env` |
||||||
|
and edit it according to your database setup. You'll need to create a database |
||||||
|
(likely names `testfixtures_test`) before continuing. Then run the command |
||||||
|
for the database you want to run tests against: |
||||||
|
|
||||||
|
```bash |
||||||
|
task test:pg # PostgreSQL |
||||||
|
task test:mysql # MySQL |
||||||
|
task test:sqlite # SQLite |
||||||
|
task test:sqlserver # Microsoft SQL Server |
||||||
|
``` |
||||||
|
|
||||||
|
GitHub Actions (CI) runs the same Docker setup available locally. |
||||||
|
|
||||||
|
## Alternatives |
||||||
|
|
||||||
|
If you don't think using fixtures is a good idea, you can try one of these |
||||||
|
packages instead: |
||||||
|
|
||||||
|
- [factory-go][factorygo]: Factory for Go. Inspired by Python's Factory Boy |
||||||
|
and Ruby's Factory Girl |
||||||
|
- [go-txdb (Single transaction SQL driver for Go)][gotxdb]: Use a single |
||||||
|
database transaction for each functional test, so you can rollback to |
||||||
|
previous state between tests to have the same database state in all tests |
||||||
|
- [go-sqlmock][gosqlmock]: A mock for the sql.DB interface. This allow you to |
||||||
|
unit test database code without having to connect to a real database |
||||||
|
- [dbcleaner][dbcleaner] - Clean database for testing, inspired by |
||||||
|
database_cleaner for Ruby |
||||||
|
|
||||||
|
[doc]: https://pkg.go.dev/github.com/go-testfixtures/testfixtures/v3?tab=doc |
||||||
|
[railstests]: http://guides.rubyonrails.org/testing.html#the-test-database |
||||||
|
[gotxdb]: https://github.com/DATA-DOG/go-txdb |
||||||
|
[gosqlmock]: https://github.com/DATA-DOG/go-sqlmock |
||||||
|
[factorygo]: https://github.com/bluele/factory-go |
||||||
|
[dbcleaner]: https://github.com/khaiql/dbcleaner |
@ -0,0 +1,59 @@ |
|||||||
|
# https://taskfile.org |
||||||
|
|
||||||
|
version: '2' |
||||||
|
|
||||||
|
tasks: |
||||||
|
build: |
||||||
|
cmds: |
||||||
|
- go build -v -tags sqlite -o ./testfixtures{{exeExt}} ./cmd/testfixtures |
||||||
|
|
||||||
|
test-cli: |
||||||
|
cmds: |
||||||
|
- ./testfixtures -d sqlite -c testdb.sqlite3 -D testdata/fixtures |
||||||
|
|
||||||
|
test:pg: |
||||||
|
desc: Test PostgreSQL |
||||||
|
cmds: |
||||||
|
- task: test-db |
||||||
|
vars: {DATABASE: postgresql} |
||||||
|
|
||||||
|
test:mysql: |
||||||
|
desc: Test MySQL |
||||||
|
cmds: |
||||||
|
- task: test:db |
||||||
|
vars: {DATABASE: mysql} |
||||||
|
|
||||||
|
test:sqlite: |
||||||
|
desc: Test SQLite |
||||||
|
cmds: |
||||||
|
- task: test-db |
||||||
|
vars: {DATABASE: sqlite} |
||||||
|
|
||||||
|
test:sqlserver: |
||||||
|
desc: Test SQLServer |
||||||
|
cmds: |
||||||
|
- task: test-db |
||||||
|
vars: {DATABASE: sqlserver} |
||||||
|
|
||||||
|
test-db: |
||||||
|
cmds: |
||||||
|
- go test -v -tags {{.DATABASE}} |
||||||
|
|
||||||
|
goreleaser:test: |
||||||
|
desc: Tests release process without publishing |
||||||
|
cmds: |
||||||
|
- goreleaser --snapshot --rm-dist |
||||||
|
|
||||||
|
docker: |
||||||
|
cmds: |
||||||
|
- task: docker:build |
||||||
|
- task: docker:test |
||||||
|
|
||||||
|
docker:build: |
||||||
|
cmds: |
||||||
|
- docker build -t testfixtures . |
||||||
|
|
||||||
|
docker:test: |
||||||
|
cmds: |
||||||
|
- docker-compose down -v |
||||||
|
- docker-compose run testfixtures go test -v -tags 'postgresql sqlite mysql sqlserver' |
@ -0,0 +1,37 @@ |
|||||||
|
version: '3' |
||||||
|
|
||||||
|
services: |
||||||
|
testfixtures: |
||||||
|
image: testfixtures |
||||||
|
depends_on: |
||||||
|
- postgresql |
||||||
|
- mysql |
||||||
|
- sqlserver |
||||||
|
environment: |
||||||
|
PGPASSWORD: postgres |
||||||
|
PG_CONN_STRING: host=postgresql user=postgres dbname=testfixtures_test port=5432 sslmode=disable |
||||||
|
|
||||||
|
MYSQL_CONN_STRING: root:mysql@tcp(mysql)/testfixtures_test?multiStatements=true |
||||||
|
|
||||||
|
SQLITE_CONN_STRING: testfixtures_test.sqlite3 |
||||||
|
|
||||||
|
SQLSERVER_CONN_STRING: server=sqlserver;database=master;user id=sa;password=SQL@1server;encrypt=disable |
||||||
|
|
||||||
|
postgresql: |
||||||
|
image: postgres:12.1-alpine |
||||||
|
environment: |
||||||
|
POSTGRES_DB: testfixtures_test |
||||||
|
POSTGRES_USER: postgres |
||||||
|
POSTGRES_PASSWORD: postgres |
||||||
|
|
||||||
|
mysql: |
||||||
|
image: mysql:8.0 |
||||||
|
environment: |
||||||
|
MYSQL_DATABASE: testfixtures_test |
||||||
|
MYSQL_ROOT_PASSWORD: mysql |
||||||
|
|
||||||
|
sqlserver: |
||||||
|
image: mcr.microsoft.com/mssql/server:2019-latest |
||||||
|
environment: |
||||||
|
ACCEPT_EULA: 'Y' |
||||||
|
SA_PASSWORD: SQL@1server |
@ -0,0 +1,165 @@ |
|||||||
|
package testfixtures |
||||||
|
|
||||||
|
import ( |
||||||
|
"database/sql" |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"unicode/utf8" |
||||||
|
|
||||||
|
"gopkg.in/yaml.v2" |
||||||
|
) |
||||||
|
|
||||||
|
// Dumper is resposible for dumping fixtures from the database into a
|
||||||
|
// directory.
|
||||||
|
type Dumper struct { |
||||||
|
db *sql.DB |
||||||
|
helper helper |
||||||
|
dir string |
||||||
|
|
||||||
|
tables []string |
||||||
|
} |
||||||
|
|
||||||
|
// NewDumper creates a new dumper with the given options.
|
||||||
|
//
|
||||||
|
// The "DumpDatabase", "DumpDialect" and "DumpDirectory" options are required.
|
||||||
|
func NewDumper(options ...func(*Dumper) error) (*Dumper, error) { |
||||||
|
d := &Dumper{} |
||||||
|
|
||||||
|
for _, option := range options { |
||||||
|
if err := option(d); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return d, nil |
||||||
|
} |
||||||
|
|
||||||
|
// DumpDatabase sets the database to be dumped.
|
||||||
|
func DumpDatabase(db *sql.DB) func(*Dumper) error { |
||||||
|
return func(d *Dumper) error { |
||||||
|
d.db = db |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// DumpDialect informs Loader about which database dialect you're using.
|
||||||
|
//
|
||||||
|
// Possible options are "postgresql", "timescaledb", "mysql", "mariadb",
|
||||||
|
// "sqlite" and "sqlserver".
|
||||||
|
func DumpDialect(dialect string) func(*Dumper) error { |
||||||
|
return func(d *Dumper) error { |
||||||
|
h, err := helperForDialect(dialect) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
d.helper = h |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// DumpDirectory sets the directory where the fixtures files will be created.
|
||||||
|
func DumpDirectory(dir string) func(*Dumper) error { |
||||||
|
return func(d *Dumper) error { |
||||||
|
d.dir = dir |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// DumpTables allows you to choose which tables you want to dump.
|
||||||
|
//
|
||||||
|
// If not informed, Dumper will dump all tables by default.
|
||||||
|
func DumpTables(tables ...string) func(*Dumper) error { |
||||||
|
return func(d *Dumper) error { |
||||||
|
d.tables = tables |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Dump dumps the databases as YAML fixtures.
|
||||||
|
func (d *Dumper) Dump() error { |
||||||
|
tables := d.tables |
||||||
|
if len(tables) == 0 { |
||||||
|
var err error |
||||||
|
tables, err = d.helper.tableNames(d.db) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for _, table := range tables { |
||||||
|
if err := d.dumpTable(table); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (d *Dumper) dumpTable(table string) error { |
||||||
|
query := fmt.Sprintf("SELECT * FROM %s", d.helper.quoteKeyword(table)) |
||||||
|
|
||||||
|
stmt, err := d.db.Prepare(query) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer stmt.Close() |
||||||
|
|
||||||
|
rows, err := stmt.Query() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer rows.Close() |
||||||
|
|
||||||
|
columns, err := rows.Columns() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
fixtures := make([]yaml.MapSlice, 0, 10) |
||||||
|
for rows.Next() { |
||||||
|
entries := make([]interface{}, len(columns)) |
||||||
|
entryPtrs := make([]interface{}, len(entries)) |
||||||
|
for i := range entries { |
||||||
|
entryPtrs[i] = &entries[i] |
||||||
|
} |
||||||
|
if err := rows.Scan(entryPtrs...); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
entryMap := make([]yaml.MapItem, len(entries)) |
||||||
|
for i, column := range columns { |
||||||
|
entryMap[i] = yaml.MapItem{ |
||||||
|
Key: column, |
||||||
|
Value: convertValue(entries[i]), |
||||||
|
} |
||||||
|
} |
||||||
|
fixtures = append(fixtures, entryMap) |
||||||
|
} |
||||||
|
if err = rows.Err(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
filePath := filepath.Join(d.dir, table+".yml") |
||||||
|
f, err := os.Create(filePath) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer f.Close() |
||||||
|
|
||||||
|
data, err := yaml.Marshal(fixtures) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
_, err = f.Write(data) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
func convertValue(value interface{}) interface{} { |
||||||
|
switch v := value.(type) { |
||||||
|
case []byte: |
||||||
|
if utf8.Valid(v) { |
||||||
|
return string(v) |
||||||
|
} |
||||||
|
} |
||||||
|
return value |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
module github.com/go-testfixtures/testfixtures/v3 |
||||||
|
|
||||||
|
require ( |
||||||
|
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 |
||||||
|
github.com/go-sql-driver/mysql v1.4.1 |
||||||
|
github.com/joho/godotenv v1.3.0 |
||||||
|
github.com/lib/pq v1.3.0 |
||||||
|
github.com/mattn/go-sqlite3 v2.0.2+incompatible |
||||||
|
github.com/spf13/pflag v1.0.5 |
||||||
|
google.golang.org/appengine v1.3.0 // indirect |
||||||
|
gopkg.in/yaml.v2 v2.2.7 |
||||||
|
) |
||||||
|
|
||||||
|
go 1.13 |
@ -0,0 +1,26 @@ |
|||||||
|
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 h1:OGNva6WhsKst5OZf7eZOklDztV3hwtTHovdrLHV+MsA= |
||||||
|
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= |
||||||
|
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= |
||||||
|
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= |
||||||
|
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= |
||||||
|
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= |
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= |
||||||
|
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= |
||||||
|
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= |
||||||
|
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= |
||||||
|
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= |
||||||
|
github.com/mattn/go-sqlite3 v2.0.2+incompatible h1:qzw9c2GNT8UFrgWNDhCTqRqYUSmu/Dav/9Z58LGpk7U= |
||||||
|
github.com/mattn/go-sqlite3 v2.0.2+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= |
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= |
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= |
||||||
|
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI= |
||||||
|
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
||||||
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= |
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
||||||
|
google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk= |
||||||
|
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= |
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= |
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
||||||
|
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= |
||||||
|
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
@ -0,0 +1,599 @@ |
|||||||
|
package testfixtures // import "github.com/go-testfixtures/testfixtures/v3"
|
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"database/sql" |
||||||
|
"fmt" |
||||||
|
"io/ioutil" |
||||||
|
"os" |
||||||
|
"path" |
||||||
|
"path/filepath" |
||||||
|
"regexp" |
||||||
|
"strings" |
||||||
|
"text/template" |
||||||
|
"time" |
||||||
|
|
||||||
|
"gopkg.in/yaml.v2" |
||||||
|
) |
||||||
|
|
||||||
|
// Loader is the responsible to loading fixtures.
|
||||||
|
type Loader struct { |
||||||
|
db *sql.DB |
||||||
|
helper helper |
||||||
|
fixturesFiles []*fixtureFile |
||||||
|
|
||||||
|
skipTestDatabaseCheck bool |
||||||
|
location *time.Location |
||||||
|
|
||||||
|
template bool |
||||||
|
templateFuncs template.FuncMap |
||||||
|
templateLeftDelim string |
||||||
|
templateRightDelim string |
||||||
|
templateOptions []string |
||||||
|
templateData interface{} |
||||||
|
} |
||||||
|
|
||||||
|
type fixtureFile struct { |
||||||
|
path string |
||||||
|
fileName string |
||||||
|
content []byte |
||||||
|
insertSQLs []insertSQL |
||||||
|
} |
||||||
|
|
||||||
|
type insertSQL struct { |
||||||
|
sql string |
||||||
|
params []interface{} |
||||||
|
} |
||||||
|
|
||||||
|
var ( |
||||||
|
testDatabaseRegexp = regexp.MustCompile("(?i)test") |
||||||
|
|
||||||
|
errDatabaseIsRequired = fmt.Errorf("testfixtures: database is required") |
||||||
|
errDialectIsRequired = fmt.Errorf("testfixtures: dialect is required") |
||||||
|
) |
||||||
|
|
||||||
|
// New instantiates a new Loader instance. The "Database" and "Driver"
|
||||||
|
// options are required.
|
||||||
|
func New(options ...func(*Loader) error) (*Loader, error) { |
||||||
|
l := &Loader{ |
||||||
|
templateLeftDelim: "{{", |
||||||
|
templateRightDelim: "}}", |
||||||
|
templateOptions: []string{"missingkey=zero"}, |
||||||
|
} |
||||||
|
|
||||||
|
for _, option := range options { |
||||||
|
if err := option(l); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if l.db == nil { |
||||||
|
return nil, errDatabaseIsRequired |
||||||
|
} |
||||||
|
if l.helper == nil { |
||||||
|
return nil, errDialectIsRequired |
||||||
|
} |
||||||
|
|
||||||
|
if err := l.helper.init(l.db); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if err := l.buildInsertSQLs(); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return l, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Database sets an existing sql.DB instant to Loader.
|
||||||
|
func Database(db *sql.DB) func(*Loader) error { |
||||||
|
return func(l *Loader) error { |
||||||
|
l.db = db |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Dialect informs Loader about which database dialect you're using.
|
||||||
|
//
|
||||||
|
// Possible options are "postgresql", "timescaledb", "mysql", "mariadb",
|
||||||
|
// "sqlite" and "sqlserver".
|
||||||
|
func Dialect(dialect string) func(*Loader) error { |
||||||
|
return func(l *Loader) error { |
||||||
|
h, err := helperForDialect(dialect) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
l.helper = h |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func helperForDialect(dialect string) (helper, error) { |
||||||
|
switch dialect { |
||||||
|
case "postgres", "postgresql", "timescaledb": |
||||||
|
return &postgreSQL{}, nil |
||||||
|
case "mysql", "mariadb": |
||||||
|
return &mySQL{}, nil |
||||||
|
case "sqlite", "sqlite3": |
||||||
|
return &sqlite{}, nil |
||||||
|
case "mssql", "sqlserver": |
||||||
|
return &sqlserver{}, nil |
||||||
|
default: |
||||||
|
return nil, fmt.Errorf(`testfixtures: unrecognized dialect "%s"`, dialect) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// UseAlterConstraint If true, the contraint disabling will do
|
||||||
|
// using ALTER CONTRAINT sintax, only allowed in PG >= 9.4.
|
||||||
|
// If false, the constraint disabling will use DISABLE TRIGGER ALL,
|
||||||
|
// which requires SUPERUSER privileges.
|
||||||
|
//
|
||||||
|
// Only valid for PostgreSQL. Returns an error otherwise.
|
||||||
|
func UseAlterConstraint() func(*Loader) error { |
||||||
|
return func(l *Loader) error { |
||||||
|
pgHelper, ok := l.helper.(*postgreSQL) |
||||||
|
if !ok { |
||||||
|
return fmt.Errorf("testfixtures: UseAlterConstraint is only valid for PostgreSQL databases") |
||||||
|
} |
||||||
|
pgHelper.useAlterConstraint = true |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// SkipResetSequences prevents Loader from reseting sequences after loading
|
||||||
|
// fixtures.
|
||||||
|
//
|
||||||
|
// Only valid for PostgreSQL. Returns an error otherwise.
|
||||||
|
func SkipResetSequences() func(*Loader) error { |
||||||
|
return func(l *Loader) error { |
||||||
|
pgHelper, ok := l.helper.(*postgreSQL) |
||||||
|
if !ok { |
||||||
|
return fmt.Errorf("testfixtures: SkipResetSequences is only valid for PostgreSQL databases") |
||||||
|
} |
||||||
|
pgHelper.skipResetSequences = true |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// ResetSequencesTo sets the value the sequences will be reset to.
|
||||||
|
//
|
||||||
|
// Defaults to 10000.
|
||||||
|
//
|
||||||
|
// Only valid for PostgreSQL. Returns an error otherwise.
|
||||||
|
func ResetSequencesTo(value int64) func(*Loader) error { |
||||||
|
return func(l *Loader) error { |
||||||
|
pgHelper, ok := l.helper.(*postgreSQL) |
||||||
|
if !ok { |
||||||
|
return fmt.Errorf("testfixtures: ResetSequencesTo is only valid for PostgreSQL databases") |
||||||
|
} |
||||||
|
pgHelper.resetSequencesTo = value |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// DangerousSkipTestDatabaseCheck will make Loader not check if the database
|
||||||
|
// name contains "test". Use with caution!
|
||||||
|
func DangerousSkipTestDatabaseCheck() func(*Loader) error { |
||||||
|
return func(l *Loader) error { |
||||||
|
l.skipTestDatabaseCheck = true |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Directory informs Loader to load YAML files from a given directory.
|
||||||
|
func Directory(dir string) func(*Loader) error { |
||||||
|
return func(l *Loader) error { |
||||||
|
fixtures, err := l.fixturesFromDir(dir) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
l.fixturesFiles = append(l.fixturesFiles, fixtures...) |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Files informs Loader to load a given set of YAML files.
|
||||||
|
func Files(files ...string) func(*Loader) error { |
||||||
|
return func(l *Loader) error { |
||||||
|
fixtures, err := l.fixturesFromFiles(files...) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
l.fixturesFiles = append(l.fixturesFiles, fixtures...) |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Paths inform Loader to load a given set of YAML files and directories.
|
||||||
|
func Paths(paths ...string) func(*Loader) error { |
||||||
|
return func(l *Loader) error { |
||||||
|
fixtures, err := l.fixturesFromPaths(paths...) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
l.fixturesFiles = append(l.fixturesFiles, fixtures...) |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Location makes Loader use the given location by default when parsing
|
||||||
|
// dates. If not given, by default it uses the value of time.Local.
|
||||||
|
func Location(location *time.Location) func(*Loader) error { |
||||||
|
return func(l *Loader) error { |
||||||
|
l.location = location |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Template makes loader process each YAML file as an template using the
|
||||||
|
// text/template package.
|
||||||
|
//
|
||||||
|
// For more information on how templates work in Go please read:
|
||||||
|
// https://golang.org/pkg/text/template/
|
||||||
|
//
|
||||||
|
// If not given the YAML files are parsed as is.
|
||||||
|
func Template() func(*Loader) error { |
||||||
|
return func(l *Loader) error { |
||||||
|
l.template = true |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// TemplateFuncs allow choosing which functions will be available
|
||||||
|
// when processing templates.
|
||||||
|
//
|
||||||
|
// For more information see: https://golang.org/pkg/text/template/#Template.Funcs
|
||||||
|
func TemplateFuncs(funcs template.FuncMap) func(*Loader) error { |
||||||
|
return func(l *Loader) error { |
||||||
|
if !l.template { |
||||||
|
return fmt.Errorf(`testfixtures: the Template() options is required in order to use the TemplateFuns() option`) |
||||||
|
} |
||||||
|
|
||||||
|
l.templateFuncs = funcs |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// TemplateDelims allow choosing which delimiters will be used for templating.
|
||||||
|
// This defaults to "{{" and "}}".
|
||||||
|
//
|
||||||
|
// For more information see https://golang.org/pkg/text/template/#Template.Delims
|
||||||
|
func TemplateDelims(left, right string) func(*Loader) error { |
||||||
|
return func(l *Loader) error { |
||||||
|
if !l.template { |
||||||
|
return fmt.Errorf(`testfixtures: the Template() options is required in order to use the TemplateDelims() option`) |
||||||
|
} |
||||||
|
|
||||||
|
l.templateLeftDelim = left |
||||||
|
l.templateRightDelim = right |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// TemplateOptions allows you to specific which text/template options will
|
||||||
|
// be enabled when processing templates.
|
||||||
|
//
|
||||||
|
// This defaults to "missingkey=zero". Check the available options here:
|
||||||
|
// https://golang.org/pkg/text/template/#Template.Option
|
||||||
|
func TemplateOptions(options ...string) func(*Loader) error { |
||||||
|
return func(l *Loader) error { |
||||||
|
if !l.template { |
||||||
|
return fmt.Errorf(`testfixtures: the Template() options is required in order to use the TemplateOptions() option`) |
||||||
|
} |
||||||
|
|
||||||
|
l.templateOptions = options |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// TemplateData allows you to specify which data will be available
|
||||||
|
// when processing templates. Data is accesible by prefixing it with a "."
|
||||||
|
// like {{.MyKey}}.
|
||||||
|
func TemplateData(data interface{}) func(*Loader) error { |
||||||
|
return func(l *Loader) error { |
||||||
|
if !l.template { |
||||||
|
return fmt.Errorf(`testfixtures: the Template() options is required in order to use the TemplateData() option`) |
||||||
|
} |
||||||
|
|
||||||
|
l.templateData = data |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// EnsureTestDatabase returns an error if the database name does not contains
|
||||||
|
// "test".
|
||||||
|
func (l *Loader) EnsureTestDatabase() error { |
||||||
|
dbName, err := l.helper.databaseName(l.db) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if !testDatabaseRegexp.MatchString(dbName) { |
||||||
|
return fmt.Errorf(`testfixtures: database "%s" does not appear to be a test database`, dbName) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Load wipes and after load all fixtures in the database.
|
||||||
|
// if err := fixtures.Load(); err != nil {
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
|
func (l *Loader) Load() error { |
||||||
|
if !l.skipTestDatabaseCheck { |
||||||
|
if err := l.EnsureTestDatabase(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
err := l.helper.disableReferentialIntegrity(l.db, func(tx *sql.Tx) error { |
||||||
|
for _, file := range l.fixturesFiles { |
||||||
|
modified, err := l.helper.isTableModified(tx, file.fileNameWithoutExtension()) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if !modified { |
||||||
|
continue |
||||||
|
} |
||||||
|
if err := file.delete(tx, l.helper); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
err = l.helper.whileInsertOnTable(tx, file.fileNameWithoutExtension(), func() error { |
||||||
|
for j, i := range file.insertSQLs { |
||||||
|
if _, err := tx.Exec(i.sql, i.params...); err != nil { |
||||||
|
return &InsertError{ |
||||||
|
Err: err, |
||||||
|
File: file.fileName, |
||||||
|
Index: j, |
||||||
|
SQL: i.sql, |
||||||
|
Params: i.params, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
}) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
}) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return l.helper.afterLoad(l.db) |
||||||
|
} |
||||||
|
|
||||||
|
// InsertError will be returned if any error happens on database while
|
||||||
|
// inserting the record.
|
||||||
|
type InsertError struct { |
||||||
|
Err error |
||||||
|
File string |
||||||
|
Index int |
||||||
|
SQL string |
||||||
|
Params []interface{} |
||||||
|
} |
||||||
|
|
||||||
|
func (e *InsertError) Error() string { |
||||||
|
return fmt.Sprintf( |
||||||
|
"testfixtures: error inserting record: %v, on file: %s, index: %d, sql: %s, params: %v", |
||||||
|
e.Err, |
||||||
|
e.File, |
||||||
|
e.Index, |
||||||
|
e.SQL, |
||||||
|
e.Params, |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
func (l *Loader) buildInsertSQLs() error { |
||||||
|
for _, f := range l.fixturesFiles { |
||||||
|
var records interface{} |
||||||
|
if err := yaml.Unmarshal(f.content, &records); err != nil { |
||||||
|
return fmt.Errorf("testfixtures: could not unmarshal YAML: %w", err) |
||||||
|
} |
||||||
|
|
||||||
|
switch records := records.(type) { |
||||||
|
case []interface{}: |
||||||
|
f.insertSQLs = make([]insertSQL, 0, len(records)) |
||||||
|
|
||||||
|
for _, record := range records { |
||||||
|
recordMap, ok := record.(map[interface{}]interface{}) |
||||||
|
if !ok { |
||||||
|
return fmt.Errorf("testfixtures: could not cast record: not a map[interface{}]interface{}") |
||||||
|
} |
||||||
|
|
||||||
|
sql, values, err := l.buildInsertSQL(f, recordMap) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
f.insertSQLs = append(f.insertSQLs, insertSQL{sql, values}) |
||||||
|
} |
||||||
|
case map[interface{}]interface{}: |
||||||
|
f.insertSQLs = make([]insertSQL, 0, len(records)) |
||||||
|
|
||||||
|
for _, record := range records { |
||||||
|
recordMap, ok := record.(map[interface{}]interface{}) |
||||||
|
if !ok { |
||||||
|
return fmt.Errorf("testfixtures: could not cast record: not a map[interface{}]interface{}") |
||||||
|
} |
||||||
|
|
||||||
|
sql, values, err := l.buildInsertSQL(f, recordMap) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
f.insertSQLs = append(f.insertSQLs, insertSQL{sql, values}) |
||||||
|
} |
||||||
|
default: |
||||||
|
return fmt.Errorf("testfixtures: fixture is not a slice or map") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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 { |
||||||
|
if _, err := tx.Exec(fmt.Sprintf("DELETE FROM %s", h.quoteKeyword(f.fileNameWithoutExtension()))); err != nil { |
||||||
|
return fmt.Errorf(`testfixtures: could not clean table "%s": %w`, f.fileNameWithoutExtension(), err) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (l *Loader) buildInsertSQL(f *fixtureFile, record map[interface{}]interface{}) (sqlStr string, values []interface{}, err error) { |
||||||
|
var ( |
||||||
|
sqlColumns = make([]string, 0, len(record)) |
||||||
|
sqlValues = make([]string, 0, len(record)) |
||||||
|
i = 1 |
||||||
|
) |
||||||
|
for key, value := range record { |
||||||
|
keyStr, ok := key.(string) |
||||||
|
if !ok { |
||||||
|
err = fmt.Errorf("testfixtures: record map key is not a string") |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
sqlColumns = append(sqlColumns, l.helper.quoteKeyword(keyStr)) |
||||||
|
|
||||||
|
// 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 := l.tryStrToDate(v); err == nil { |
||||||
|
value = t |
||||||
|
} |
||||||
|
case []interface{}, map[interface{}]interface{}: |
||||||
|
value = recursiveToJSON(v) |
||||||
|
} |
||||||
|
|
||||||
|
switch l.helper.paramType() { |
||||||
|
case paramTypeDollar: |
||||||
|
sqlValues = append(sqlValues, fmt.Sprintf("$%d", i)) |
||||||
|
case paramTypeQuestion: |
||||||
|
sqlValues = append(sqlValues, "?") |
||||||
|
case paramTypeAtSign: |
||||||
|
sqlValues = append(sqlValues, fmt.Sprintf("@p%d", i)) |
||||||
|
} |
||||||
|
|
||||||
|
values = append(values, value) |
||||||
|
i++ |
||||||
|
} |
||||||
|
|
||||||
|
sqlStr = fmt.Sprintf( |
||||||
|
"INSERT INTO %s (%s) VALUES (%s)", |
||||||
|
l.helper.quoteKeyword(f.fileNameWithoutExtension()), |
||||||
|
strings.Join(sqlColumns, ", "), |
||||||
|
strings.Join(sqlValues, ", "), |
||||||
|
) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (l *Loader) fixturesFromDir(dir string) ([]*fixtureFile, error) { |
||||||
|
fileinfos, err := ioutil.ReadDir(dir) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf(`testfixtures: could not stat directory "%s": %w`, dir, err) |
||||||
|
} |
||||||
|
|
||||||
|
files := make([]*fixtureFile, 0, len(fileinfos)) |
||||||
|
|
||||||
|
for _, fileinfo := range fileinfos { |
||||||
|
fileExt := filepath.Ext(fileinfo.Name()) |
||||||
|
if !fileinfo.IsDir() && (fileExt == ".yml" || fileExt == ".yaml") { |
||||||
|
fixture := &fixtureFile{ |
||||||
|
path: path.Join(dir, fileinfo.Name()), |
||||||
|
fileName: fileinfo.Name(), |
||||||
|
} |
||||||
|
fixture.content, err = ioutil.ReadFile(fixture.path) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf(`testfixtures: could not read file "%s": %w`, fixture.path, err) |
||||||
|
} |
||||||
|
if err := l.processFileTemplate(fixture); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
files = append(files, fixture) |
||||||
|
} |
||||||
|
} |
||||||
|
return files, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (l *Loader) fixturesFromFiles(fileNames ...string) ([]*fixtureFile, error) { |
||||||
|
var ( |
||||||
|
fixtureFiles = make([]*fixtureFile, 0, len(fileNames)) |
||||||
|
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, fmt.Errorf(`testfixtures: could not read file "%s": %w`, fixture.path, err) |
||||||
|
} |
||||||
|
if err := l.processFileTemplate(fixture); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
fixtureFiles = append(fixtureFiles, fixture) |
||||||
|
} |
||||||
|
|
||||||
|
return fixtureFiles, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (l *Loader) fixturesFromPaths(paths ...string) ([]*fixtureFile, error) { |
||||||
|
fixtureExtractor := func(p string, isDir bool) ([]*fixtureFile, error) { |
||||||
|
if isDir { |
||||||
|
return l.fixturesFromDir(p) |
||||||
|
} |
||||||
|
|
||||||
|
return l.fixturesFromFiles(p) |
||||||
|
} |
||||||
|
|
||||||
|
var fixtureFiles []*fixtureFile |
||||||
|
|
||||||
|
for _, p := range paths { |
||||||
|
f, err := os.Stat(p) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf(`testfixtures: could not stat path "%s": %w`, p, err) |
||||||
|
} |
||||||
|
|
||||||
|
fixtures, err := fixtureExtractor(p, f.IsDir()) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
fixtureFiles = append(fixtureFiles, fixtures...) |
||||||
|
} |
||||||
|
|
||||||
|
return fixtureFiles, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (l *Loader) processFileTemplate(f *fixtureFile) error { |
||||||
|
if !l.template { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
t := template.New(""). |
||||||
|
Funcs(l.templateFuncs). |
||||||
|
Delims(l.templateLeftDelim, l.templateRightDelim). |
||||||
|
Option(l.templateOptions...) |
||||||
|
t, err := t.Parse(string(f.content)) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf(`textfixtures: error on parsing template in %s: %w`, f.fileName, err) |
||||||
|
} |
||||||
|
|
||||||
|
var buffer bytes.Buffer |
||||||
|
if err := t.Execute(&buffer, l.templateData); err != nil { |
||||||
|
return fmt.Errorf(`textfixtures: error on executing template in %s: %w`, f.fileName, err) |
||||||
|
} |
||||||
|
|
||||||
|
f.content = buffer.Bytes() |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
package testfixtures |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
var timeFormats = [...]string{ |
||||||
|
"2006-01-02", |
||||||
|
"2006-01-02 15:04", |
||||||
|
"2006-01-02 15:04:05", |
||||||
|
"20060102", |
||||||
|
"20060102 15:04", |
||||||
|
"20060102 15:04:05", |
||||||
|
"02/01/2006", |
||||||
|
"02/01/2006 15:04", |
||||||
|
"02/01/2006 15:04:05", |
||||||
|
"2006-01-02T15:04-07:00", |
||||||
|
"2006-01-02T15:04:05-07:00", |
||||||
|
"2006-01-02T15:04:05Z07:00", |
||||||
|
"2006-01-02 15:04:05Z07:00", |
||||||
|
"2006-01-02T15:04:05Z0700", |
||||||
|
"2006-01-02 15:04:05Z0700", |
||||||
|
"2006-01-02T15:04:05Z07", |
||||||
|
"2006-01-02 15:04:05Z07", |
||||||
|
"2006-01-02 15:04:05 MST", |
||||||
|
} |
||||||
|
|
||||||
|
func (l *Loader) tryStrToDate(s string) (time.Time, error) { |
||||||
|
loc := l.location |
||||||
|
if loc == nil { |
||||||
|
loc = time.Local |
||||||
|
} |
||||||
|
|
||||||
|
for _, f := range timeFormats { |
||||||
|
t, err := time.ParseInLocation(f, s, loc) |
||||||
|
if err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
return t, nil |
||||||
|
} |
||||||
|
return time.Time{}, fmt.Errorf(`testfixtures: could not convert string "%s" to time`, s) |
||||||
|
} |
@ -0,0 +1,299 @@ |
|||||||
|
// Extracted from Go database/sql source code
|
||||||
|
|
||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Type conversions for Scan.
|
||||||
|
|
||||||
|
package sqlite3 |
||||||
|
|
||||||
|
import ( |
||||||
|
"database/sql" |
||||||
|
"database/sql/driver" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"reflect" |
||||||
|
"strconv" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
var errNilPtr = errors.New("destination pointer is nil") // embedded in descriptive error
|
||||||
|
|
||||||
|
// convertAssign copies to dest the value in src, converting it if possible.
|
||||||
|
// An error is returned if the copy would result in loss of information.
|
||||||
|
// dest should be a pointer type.
|
||||||
|
func convertAssign(dest, src interface{}) error { |
||||||
|
// Common cases, without reflect.
|
||||||
|
switch s := src.(type) { |
||||||
|
case string: |
||||||
|
switch d := dest.(type) { |
||||||
|
case *string: |
||||||
|
if d == nil { |
||||||
|
return errNilPtr |
||||||
|
} |
||||||
|
*d = s |
||||||
|
return nil |
||||||
|
case *[]byte: |
||||||
|
if d == nil { |
||||||
|
return errNilPtr |
||||||
|
} |
||||||
|
*d = []byte(s) |
||||||
|
return nil |
||||||
|
case *sql.RawBytes: |
||||||
|
if d == nil { |
||||||
|
return errNilPtr |
||||||
|
} |
||||||
|
*d = append((*d)[:0], s...) |
||||||
|
return nil |
||||||
|
} |
||||||
|
case []byte: |
||||||
|
switch d := dest.(type) { |
||||||
|
case *string: |
||||||
|
if d == nil { |
||||||
|
return errNilPtr |
||||||
|
} |
||||||
|
*d = string(s) |
||||||
|
return nil |
||||||
|
case *interface{}: |
||||||
|
if d == nil { |
||||||
|
return errNilPtr |
||||||
|
} |
||||||
|
*d = cloneBytes(s) |
||||||
|
return nil |
||||||
|
case *[]byte: |
||||||
|
if d == nil { |
||||||
|
return errNilPtr |
||||||
|
} |
||||||
|
*d = cloneBytes(s) |
||||||
|
return nil |
||||||
|
case *sql.RawBytes: |
||||||
|
if d == nil { |
||||||
|
return errNilPtr |
||||||
|
} |
||||||
|
*d = s |
||||||
|
return nil |
||||||
|
} |
||||||
|
case time.Time: |
||||||
|
switch d := dest.(type) { |
||||||
|
case *time.Time: |
||||||
|
*d = s |
||||||
|
return nil |
||||||
|
case *string: |
||||||
|
*d = s.Format(time.RFC3339Nano) |
||||||
|
return nil |
||||||
|
case *[]byte: |
||||||
|
if d == nil { |
||||||
|
return errNilPtr |
||||||
|
} |
||||||
|
*d = []byte(s.Format(time.RFC3339Nano)) |
||||||
|
return nil |
||||||
|
case *sql.RawBytes: |
||||||
|
if d == nil { |
||||||
|
return errNilPtr |
||||||
|
} |
||||||
|
*d = s.AppendFormat((*d)[:0], time.RFC3339Nano) |
||||||
|
return nil |
||||||
|
} |
||||||
|
case nil: |
||||||
|
switch d := dest.(type) { |
||||||
|
case *interface{}: |
||||||
|
if d == nil { |
||||||
|
return errNilPtr |
||||||
|
} |
||||||
|
*d = nil |
||||||
|
return nil |
||||||
|
case *[]byte: |
||||||
|
if d == nil { |
||||||
|
return errNilPtr |
||||||
|
} |
||||||
|
*d = nil |
||||||
|
return nil |
||||||
|
case *sql.RawBytes: |
||||||
|
if d == nil { |
||||||
|
return errNilPtr |
||||||
|
} |
||||||
|
*d = nil |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var sv reflect.Value |
||||||
|
|
||||||
|
switch d := dest.(type) { |
||||||
|
case *string: |
||||||
|
sv = reflect.ValueOf(src) |
||||||
|
switch sv.Kind() { |
||||||
|
case reflect.Bool, |
||||||
|
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, |
||||||
|
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, |
||||||
|
reflect.Float32, reflect.Float64: |
||||||
|
*d = asString(src) |
||||||
|
return nil |
||||||
|
} |
||||||
|
case *[]byte: |
||||||
|
sv = reflect.ValueOf(src) |
||||||
|
if b, ok := asBytes(nil, sv); ok { |
||||||
|
*d = b |
||||||
|
return nil |
||||||
|
} |
||||||
|
case *sql.RawBytes: |
||||||
|
sv = reflect.ValueOf(src) |
||||||
|
if b, ok := asBytes([]byte(*d)[:0], sv); ok { |
||||||
|
*d = sql.RawBytes(b) |
||||||
|
return nil |
||||||
|
} |
||||||
|
case *bool: |
||||||
|
bv, err := driver.Bool.ConvertValue(src) |
||||||
|
if err == nil { |
||||||
|
*d = bv.(bool) |
||||||
|
} |
||||||
|
return err |
||||||
|
case *interface{}: |
||||||
|
*d = src |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
if scanner, ok := dest.(sql.Scanner); ok { |
||||||
|
return scanner.Scan(src) |
||||||
|
} |
||||||
|
|
||||||
|
dpv := reflect.ValueOf(dest) |
||||||
|
if dpv.Kind() != reflect.Ptr { |
||||||
|
return errors.New("destination not a pointer") |
||||||
|
} |
||||||
|
if dpv.IsNil() { |
||||||
|
return errNilPtr |
||||||
|
} |
||||||
|
|
||||||
|
if !sv.IsValid() { |
||||||
|
sv = reflect.ValueOf(src) |
||||||
|
} |
||||||
|
|
||||||
|
dv := reflect.Indirect(dpv) |
||||||
|
if sv.IsValid() && sv.Type().AssignableTo(dv.Type()) { |
||||||
|
switch b := src.(type) { |
||||||
|
case []byte: |
||||||
|
dv.Set(reflect.ValueOf(cloneBytes(b))) |
||||||
|
default: |
||||||
|
dv.Set(sv) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
if dv.Kind() == sv.Kind() && sv.Type().ConvertibleTo(dv.Type()) { |
||||||
|
dv.Set(sv.Convert(dv.Type())) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// The following conversions use a string value as an intermediate representation
|
||||||
|
// to convert between various numeric types.
|
||||||
|
//
|
||||||
|
// This also allows scanning into user defined types such as "type Int int64".
|
||||||
|
// For symmetry, also check for string destination types.
|
||||||
|
switch dv.Kind() { |
||||||
|
case reflect.Ptr: |
||||||
|
if src == nil { |
||||||
|
dv.Set(reflect.Zero(dv.Type())) |
||||||
|
return nil |
||||||
|
} |
||||||
|
dv.Set(reflect.New(dv.Type().Elem())) |
||||||
|
return convertAssign(dv.Interface(), src) |
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
||||||
|
s := asString(src) |
||||||
|
i64, err := strconv.ParseInt(s, 10, dv.Type().Bits()) |
||||||
|
if err != nil { |
||||||
|
err = strconvErr(err) |
||||||
|
return fmt.Errorf("converting driver.Value type %T (%q) to a %s: %v", src, s, dv.Kind(), err) |
||||||
|
} |
||||||
|
dv.SetInt(i64) |
||||||
|
return nil |
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: |
||||||
|
s := asString(src) |
||||||
|
u64, err := strconv.ParseUint(s, 10, dv.Type().Bits()) |
||||||
|
if err != nil { |
||||||
|
err = strconvErr(err) |
||||||
|
return fmt.Errorf("converting driver.Value type %T (%q) to a %s: %v", src, s, dv.Kind(), err) |
||||||
|
} |
||||||
|
dv.SetUint(u64) |
||||||
|
return nil |
||||||
|
case reflect.Float32, reflect.Float64: |
||||||
|
s := asString(src) |
||||||
|
f64, err := strconv.ParseFloat(s, dv.Type().Bits()) |
||||||
|
if err != nil { |
||||||
|
err = strconvErr(err) |
||||||
|
return fmt.Errorf("converting driver.Value type %T (%q) to a %s: %v", src, s, dv.Kind(), err) |
||||||
|
} |
||||||
|
dv.SetFloat(f64) |
||||||
|
return nil |
||||||
|
case reflect.String: |
||||||
|
switch v := src.(type) { |
||||||
|
case string: |
||||||
|
dv.SetString(v) |
||||||
|
return nil |
||||||
|
case []byte: |
||||||
|
dv.SetString(string(v)) |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return fmt.Errorf("unsupported Scan, storing driver.Value type %T into type %T", src, dest) |
||||||
|
} |
||||||
|
|
||||||
|
func strconvErr(err error) error { |
||||||
|
if ne, ok := err.(*strconv.NumError); ok { |
||||||
|
return ne.Err |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
func cloneBytes(b []byte) []byte { |
||||||
|
if b == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
c := make([]byte, len(b)) |
||||||
|
copy(c, b) |
||||||
|
return c |
||||||
|
} |
||||||
|
|
||||||
|
func asString(src interface{}) string { |
||||||
|
switch v := src.(type) { |
||||||
|
case string: |
||||||
|
return v |
||||||
|
case []byte: |
||||||
|
return string(v) |
||||||
|
} |
||||||
|
rv := reflect.ValueOf(src) |
||||||
|
switch rv.Kind() { |
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
||||||
|
return strconv.FormatInt(rv.Int(), 10) |
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: |
||||||
|
return strconv.FormatUint(rv.Uint(), 10) |
||||||
|
case reflect.Float64: |
||||||
|
return strconv.FormatFloat(rv.Float(), 'g', -1, 64) |
||||||
|
case reflect.Float32: |
||||||
|
return strconv.FormatFloat(rv.Float(), 'g', -1, 32) |
||||||
|
case reflect.Bool: |
||||||
|
return strconv.FormatBool(rv.Bool()) |
||||||
|
} |
||||||
|
return fmt.Sprintf("%v", src) |
||||||
|
} |
||||||
|
|
||||||
|
func asBytes(buf []byte, rv reflect.Value) (b []byte, ok bool) { |
||||||
|
switch rv.Kind() { |
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
||||||
|
return strconv.AppendInt(buf, rv.Int(), 10), true |
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: |
||||||
|
return strconv.AppendUint(buf, rv.Uint(), 10), true |
||||||
|
case reflect.Float32: |
||||||
|
return strconv.AppendFloat(buf, rv.Float(), 'g', -1, 32), true |
||||||
|
case reflect.Float64: |
||||||
|
return strconv.AppendFloat(buf, rv.Float(), 'g', -1, 64), true |
||||||
|
case reflect.Bool: |
||||||
|
return strconv.AppendBool(buf, rv.Bool()), true |
||||||
|
case reflect.String: |
||||||
|
s := rv.String() |
||||||
|
return append(buf, s...), true |
||||||
|
} |
||||||
|
return |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,20 @@ |
|||||||
|
// Copyright (C) 2019 G.J.R. Timmer <gjr.timmer@gmail.com>.
|
||||||
|
// Copyright (C) 2018 segment.com <friends@segment.com>
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by an MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build cgo
|
||||||
|
|
||||||
|
package sqlite3 |
||||||
|
|
||||||
|
// SQLitePreUpdateData represents all of the data available during a
|
||||||
|
// pre-update hook call.
|
||||||
|
type SQLitePreUpdateData struct { |
||||||
|
Conn *SQLiteConn |
||||||
|
Op int |
||||||
|
DatabaseName string |
||||||
|
TableName string |
||||||
|
OldRowID int64 |
||||||
|
NewRowID int64 |
||||||
|
} |
@ -0,0 +1,112 @@ |
|||||||
|
// Copyright (C) 2019 G.J.R. Timmer <gjr.timmer@gmail.com>.
|
||||||
|
// Copyright (C) 2018 segment.com <friends@segment.com>
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by an MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build sqlite_preupdate_hook
|
||||||
|
|
||||||
|
package sqlite3 |
||||||
|
|
||||||
|
/* |
||||||
|
#cgo CFLAGS: -DSQLITE_ENABLE_PREUPDATE_HOOK |
||||||
|
#cgo LDFLAGS: -lm |
||||||
|
|
||||||
|
#ifndef USE_LIBSQLITE3 |
||||||
|
#include <sqlite3-binding.h> |
||||||
|
#else |
||||||
|
#include <sqlite3.h> |
||||||
|
#endif |
||||||
|
#include <stdlib.h> |
||||||
|
#include <string.h> |
||||||
|
|
||||||
|
void preUpdateHookTrampoline(void*, sqlite3 *, int, char *, char *, sqlite3_int64, sqlite3_int64); |
||||||
|
*/ |
||||||
|
import "C" |
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"unsafe" |
||||||
|
) |
||||||
|
|
||||||
|
// RegisterPreUpdateHook sets the pre-update hook for a connection.
|
||||||
|
//
|
||||||
|
// The callback is passed a SQLitePreUpdateData struct with the data for
|
||||||
|
// the update, as well as methods for fetching copies of impacted data.
|
||||||
|
//
|
||||||
|
// If there is an existing update hook for this connection, it will be
|
||||||
|
// removed. If callback is nil the existing hook (if any) will be removed
|
||||||
|
// without creating a new one.
|
||||||
|
func (c *SQLiteConn) RegisterPreUpdateHook(callback func(SQLitePreUpdateData)) { |
||||||
|
if callback == nil { |
||||||
|
C.sqlite3_preupdate_hook(c.db, nil, nil) |
||||||
|
} else { |
||||||
|
C.sqlite3_preupdate_hook(c.db, (*[0]byte)(unsafe.Pointer(C.preUpdateHookTrampoline)), unsafe.Pointer(newHandle(c, callback))) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Depth returns the source path of the write, see sqlite3_preupdate_depth()
|
||||||
|
func (d *SQLitePreUpdateData) Depth() int { |
||||||
|
return int(C.sqlite3_preupdate_depth(d.Conn.db)) |
||||||
|
} |
||||||
|
|
||||||
|
// Count returns the number of columns in the row
|
||||||
|
func (d *SQLitePreUpdateData) Count() int { |
||||||
|
return int(C.sqlite3_preupdate_count(d.Conn.db)) |
||||||
|
} |
||||||
|
|
||||||
|
func (d *SQLitePreUpdateData) row(dest []interface{}, new bool) error { |
||||||
|
for i := 0; i < d.Count() && i < len(dest); i++ { |
||||||
|
var val *C.sqlite3_value |
||||||
|
var src interface{} |
||||||
|
|
||||||
|
// Initially I tried making this just a function pointer argument, but
|
||||||
|
// it's absurdly complicated to pass C function pointers.
|
||||||
|
if new { |
||||||
|
C.sqlite3_preupdate_new(d.Conn.db, C.int(i), &val) |
||||||
|
} else { |
||||||
|
C.sqlite3_preupdate_old(d.Conn.db, C.int(i), &val) |
||||||
|
} |
||||||
|
|
||||||
|
switch C.sqlite3_value_type(val) { |
||||||
|
case C.SQLITE_INTEGER: |
||||||
|
src = int64(C.sqlite3_value_int64(val)) |
||||||
|
case C.SQLITE_FLOAT: |
||||||
|
src = float64(C.sqlite3_value_double(val)) |
||||||
|
case C.SQLITE_BLOB: |
||||||
|
len := C.sqlite3_value_bytes(val) |
||||||
|
blobptr := C.sqlite3_value_blob(val) |
||||||
|
src = C.GoBytes(blobptr, len) |
||||||
|
case C.SQLITE_TEXT: |
||||||
|
len := C.sqlite3_value_bytes(val) |
||||||
|
cstrptr := unsafe.Pointer(C.sqlite3_value_text(val)) |
||||||
|
src = C.GoBytes(cstrptr, len) |
||||||
|
case C.SQLITE_NULL: |
||||||
|
src = nil |
||||||
|
} |
||||||
|
|
||||||
|
err := convertAssign(&dest[i], src) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Old populates dest with the row data to be replaced. This works similar to
|
||||||
|
// database/sql's Rows.Scan()
|
||||||
|
func (d *SQLitePreUpdateData) Old(dest ...interface{}) error { |
||||||
|
if d.Op == SQLITE_INSERT { |
||||||
|
return errors.New("There is no old row for INSERT operations") |
||||||
|
} |
||||||
|
return d.row(dest, false) |
||||||
|
} |
||||||
|
|
||||||
|
// New populates dest with the replacement row data. This works similar to
|
||||||
|
// database/sql's Rows.Scan()
|
||||||
|
func (d *SQLitePreUpdateData) New(dest ...interface{}) error { |
||||||
|
if d.Op == SQLITE_DELETE { |
||||||
|
return errors.New("There is no new row for DELETE operations") |
||||||
|
} |
||||||
|
return d.row(dest, true) |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
// Copyright (C) 2019 G.J.R. Timmer <gjr.timmer@gmail.com>.
|
||||||
|
// Copyright (C) 2018 segment.com <friends@segment.com>
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by an MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !sqlite_preupdate_hook,cgo
|
||||||
|
|
||||||
|
package sqlite3 |
||||||
|
|
||||||
|
// RegisterPreUpdateHook sets the pre-update hook for a connection.
|
||||||
|
//
|
||||||
|
// The callback is passed a SQLitePreUpdateData struct with the data for
|
||||||
|
// the update, as well as methods for fetching copies of impacted data.
|
||||||
|
//
|
||||||
|
// If there is an existing update hook for this connection, it will be
|
||||||
|
// removed. If callback is nil the existing hook (if any) will be removed
|
||||||
|
// without creating a new one.
|
||||||
|
func (c *SQLiteConn) RegisterPreUpdateHook(callback func(SQLitePreUpdateData)) { |
||||||
|
// NOOP
|
||||||
|
} |
@ -0,0 +1,174 @@ |
|||||||
|
package pflag |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
// -- float32Slice Value
|
||||||
|
type float32SliceValue struct { |
||||||
|
value *[]float32 |
||||||
|
changed bool |
||||||
|
} |
||||||
|
|
||||||
|
func newFloat32SliceValue(val []float32, p *[]float32) *float32SliceValue { |
||||||
|
isv := new(float32SliceValue) |
||||||
|
isv.value = p |
||||||
|
*isv.value = val |
||||||
|
return isv |
||||||
|
} |
||||||
|
|
||||||
|
func (s *float32SliceValue) Set(val string) error { |
||||||
|
ss := strings.Split(val, ",") |
||||||
|
out := make([]float32, len(ss)) |
||||||
|
for i, d := range ss { |
||||||
|
var err error |
||||||
|
var temp64 float64 |
||||||
|
temp64, err = strconv.ParseFloat(d, 32) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
out[i] = float32(temp64) |
||||||
|
|
||||||
|
} |
||||||
|
if !s.changed { |
||||||
|
*s.value = out |
||||||
|
} else { |
||||||
|
*s.value = append(*s.value, out...) |
||||||
|
} |
||||||
|
s.changed = true |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *float32SliceValue) Type() string { |
||||||
|
return "float32Slice" |
||||||
|
} |
||||||
|
|
||||||
|
func (s *float32SliceValue) String() string { |
||||||
|
out := make([]string, len(*s.value)) |
||||||
|
for i, d := range *s.value { |
||||||
|
out[i] = fmt.Sprintf("%f", d) |
||||||
|
} |
||||||
|
return "[" + strings.Join(out, ",") + "]" |
||||||
|
} |
||||||
|
|
||||||
|
func (s *float32SliceValue) fromString(val string) (float32, error) { |
||||||
|
t64, err := strconv.ParseFloat(val, 32) |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
return float32(t64), nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *float32SliceValue) toString(val float32) string { |
||||||
|
return fmt.Sprintf("%f", val) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *float32SliceValue) Append(val string) error { |
||||||
|
i, err := s.fromString(val) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
*s.value = append(*s.value, i) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *float32SliceValue) Replace(val []string) error { |
||||||
|
out := make([]float32, len(val)) |
||||||
|
for i, d := range val { |
||||||
|
var err error |
||||||
|
out[i], err = s.fromString(d) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
*s.value = out |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *float32SliceValue) GetSlice() []string { |
||||||
|
out := make([]string, len(*s.value)) |
||||||
|
for i, d := range *s.value { |
||||||
|
out[i] = s.toString(d) |
||||||
|
} |
||||||
|
return out |
||||||
|
} |
||||||
|
|
||||||
|
func float32SliceConv(val string) (interface{}, error) { |
||||||
|
val = strings.Trim(val, "[]") |
||||||
|
// Empty string would cause a slice with one (empty) entry
|
||||||
|
if len(val) == 0 { |
||||||
|
return []float32{}, nil |
||||||
|
} |
||||||
|
ss := strings.Split(val, ",") |
||||||
|
out := make([]float32, len(ss)) |
||||||
|
for i, d := range ss { |
||||||
|
var err error |
||||||
|
var temp64 float64 |
||||||
|
temp64, err = strconv.ParseFloat(d, 32) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
out[i] = float32(temp64) |
||||||
|
|
||||||
|
} |
||||||
|
return out, nil |
||||||
|
} |
||||||
|
|
||||||
|
// GetFloat32Slice return the []float32 value of a flag with the given name
|
||||||
|
func (f *FlagSet) GetFloat32Slice(name string) ([]float32, error) { |
||||||
|
val, err := f.getFlagType(name, "float32Slice", float32SliceConv) |
||||||
|
if err != nil { |
||||||
|
return []float32{}, err |
||||||
|
} |
||||||
|
return val.([]float32), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Float32SliceVar defines a float32Slice flag with specified name, default value, and usage string.
|
||||||
|
// The argument p points to a []float32 variable in which to store the value of the flag.
|
||||||
|
func (f *FlagSet) Float32SliceVar(p *[]float32, name string, value []float32, usage string) { |
||||||
|
f.VarP(newFloat32SliceValue(value, p), name, "", usage) |
||||||
|
} |
||||||
|
|
||||||
|
// Float32SliceVarP is like Float32SliceVar, but accepts a shorthand letter that can be used after a single dash.
|
||||||
|
func (f *FlagSet) Float32SliceVarP(p *[]float32, name, shorthand string, value []float32, usage string) { |
||||||
|
f.VarP(newFloat32SliceValue(value, p), name, shorthand, usage) |
||||||
|
} |
||||||
|
|
||||||
|
// Float32SliceVar defines a float32[] flag with specified name, default value, and usage string.
|
||||||
|
// The argument p points to a float32[] variable in which to store the value of the flag.
|
||||||
|
func Float32SliceVar(p *[]float32, name string, value []float32, usage string) { |
||||||
|
CommandLine.VarP(newFloat32SliceValue(value, p), name, "", usage) |
||||||
|
} |
||||||
|
|
||||||
|
// Float32SliceVarP is like Float32SliceVar, but accepts a shorthand letter that can be used after a single dash.
|
||||||
|
func Float32SliceVarP(p *[]float32, name, shorthand string, value []float32, usage string) { |
||||||
|
CommandLine.VarP(newFloat32SliceValue(value, p), name, shorthand, usage) |
||||||
|
} |
||||||
|
|
||||||
|
// Float32Slice defines a []float32 flag with specified name, default value, and usage string.
|
||||||
|
// The return value is the address of a []float32 variable that stores the value of the flag.
|
||||||
|
func (f *FlagSet) Float32Slice(name string, value []float32, usage string) *[]float32 { |
||||||
|
p := []float32{} |
||||||
|
f.Float32SliceVarP(&p, name, "", value, usage) |
||||||
|
return &p |
||||||
|
} |
||||||
|
|
||||||
|
// Float32SliceP is like Float32Slice, but accepts a shorthand letter that can be used after a single dash.
|
||||||
|
func (f *FlagSet) Float32SliceP(name, shorthand string, value []float32, usage string) *[]float32 { |
||||||
|
p := []float32{} |
||||||
|
f.Float32SliceVarP(&p, name, shorthand, value, usage) |
||||||
|
return &p |
||||||
|
} |
||||||
|
|
||||||
|
// Float32Slice defines a []float32 flag with specified name, default value, and usage string.
|
||||||
|
// The return value is the address of a []float32 variable that stores the value of the flag.
|
||||||
|
func Float32Slice(name string, value []float32, usage string) *[]float32 { |
||||||
|
return CommandLine.Float32SliceP(name, "", value, usage) |
||||||
|
} |
||||||
|
|
||||||
|
// Float32SliceP is like Float32Slice, but accepts a shorthand letter that can be used after a single dash.
|
||||||
|
func Float32SliceP(name, shorthand string, value []float32, usage string) *[]float32 { |
||||||
|
return CommandLine.Float32SliceP(name, shorthand, value, usage) |
||||||
|
} |
@ -0,0 +1,166 @@ |
|||||||
|
package pflag |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
// -- float64Slice Value
|
||||||
|
type float64SliceValue struct { |
||||||
|
value *[]float64 |
||||||
|
changed bool |
||||||
|
} |
||||||
|
|
||||||
|
func newFloat64SliceValue(val []float64, p *[]float64) *float64SliceValue { |
||||||
|
isv := new(float64SliceValue) |
||||||
|
isv.value = p |
||||||
|
*isv.value = val |
||||||
|
return isv |
||||||
|
} |
||||||
|
|
||||||
|
func (s *float64SliceValue) Set(val string) error { |
||||||
|
ss := strings.Split(val, ",") |
||||||
|
out := make([]float64, len(ss)) |
||||||
|
for i, d := range ss { |
||||||
|
var err error |
||||||
|
out[i], err = strconv.ParseFloat(d, 64) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
if !s.changed { |
||||||
|
*s.value = out |
||||||
|
} else { |
||||||
|
*s.value = append(*s.value, out...) |
||||||
|
} |
||||||
|
s.changed = true |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *float64SliceValue) Type() string { |
||||||
|
return "float64Slice" |
||||||
|
} |
||||||
|
|
||||||
|
func (s *float64SliceValue) String() string { |
||||||
|
out := make([]string, len(*s.value)) |
||||||
|
for i, d := range *s.value { |
||||||
|
out[i] = fmt.Sprintf("%f", d) |
||||||
|
} |
||||||
|
return "[" + strings.Join(out, ",") + "]" |
||||||
|
} |
||||||
|
|
||||||
|
func (s *float64SliceValue) fromString(val string) (float64, error) { |
||||||
|
return strconv.ParseFloat(val, 64) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *float64SliceValue) toString(val float64) string { |
||||||
|
return fmt.Sprintf("%f", val) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *float64SliceValue) Append(val string) error { |
||||||
|
i, err := s.fromString(val) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
*s.value = append(*s.value, i) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *float64SliceValue) Replace(val []string) error { |
||||||
|
out := make([]float64, len(val)) |
||||||
|
for i, d := range val { |
||||||
|
var err error |
||||||
|
out[i], err = s.fromString(d) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
*s.value = out |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *float64SliceValue) GetSlice() []string { |
||||||
|
out := make([]string, len(*s.value)) |
||||||
|
for i, d := range *s.value { |
||||||
|
out[i] = s.toString(d) |
||||||
|
} |
||||||
|
return out |
||||||
|
} |
||||||
|
|
||||||
|
func float64SliceConv(val string) (interface{}, error) { |
||||||
|
val = strings.Trim(val, "[]") |
||||||
|
// Empty string would cause a slice with one (empty) entry
|
||||||
|
if len(val) == 0 { |
||||||
|
return []float64{}, nil |
||||||
|
} |
||||||
|
ss := strings.Split(val, ",") |
||||||
|
out := make([]float64, len(ss)) |
||||||
|
for i, d := range ss { |
||||||
|
var err error |
||||||
|
out[i], err = strconv.ParseFloat(d, 64) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
return out, nil |
||||||
|
} |
||||||
|
|
||||||
|
// GetFloat64Slice return the []float64 value of a flag with the given name
|
||||||
|
func (f *FlagSet) GetFloat64Slice(name string) ([]float64, error) { |
||||||
|
val, err := f.getFlagType(name, "float64Slice", float64SliceConv) |
||||||
|
if err != nil { |
||||||
|
return []float64{}, err |
||||||
|
} |
||||||
|
return val.([]float64), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Float64SliceVar defines a float64Slice flag with specified name, default value, and usage string.
|
||||||
|
// The argument p points to a []float64 variable in which to store the value of the flag.
|
||||||
|
func (f *FlagSet) Float64SliceVar(p *[]float64, name string, value []float64, usage string) { |
||||||
|
f.VarP(newFloat64SliceValue(value, p), name, "", usage) |
||||||
|
} |
||||||
|
|
||||||
|
// Float64SliceVarP is like Float64SliceVar, but accepts a shorthand letter that can be used after a single dash.
|
||||||
|
func (f *FlagSet) Float64SliceVarP(p *[]float64, name, shorthand string, value []float64, usage string) { |
||||||
|
f.VarP(newFloat64SliceValue(value, p), name, shorthand, usage) |
||||||
|
} |
||||||
|
|
||||||
|
// Float64SliceVar defines a float64[] flag with specified name, default value, and usage string.
|
||||||
|
// The argument p points to a float64[] variable in which to store the value of the flag.
|
||||||
|
func Float64SliceVar(p *[]float64, name string, value []float64, usage string) { |
||||||
|
CommandLine.VarP(newFloat64SliceValue(value, p), name, "", usage) |
||||||
|
} |
||||||
|
|
||||||
|
// Float64SliceVarP is like Float64SliceVar, but accepts a shorthand letter that can be used after a single dash.
|
||||||
|
func Float64SliceVarP(p *[]float64, name, shorthand string, value []float64, usage string) { |
||||||
|
CommandLine.VarP(newFloat64SliceValue(value, p), name, shorthand, usage) |
||||||
|
} |
||||||
|
|
||||||
|
// Float64Slice defines a []float64 flag with specified name, default value, and usage string.
|
||||||
|
// The return value is the address of a []float64 variable that stores the value of the flag.
|
||||||
|
func (f *FlagSet) Float64Slice(name string, value []float64, usage string) *[]float64 { |
||||||
|
p := []float64{} |
||||||
|
f.Float64SliceVarP(&p, name, "", value, usage) |
||||||
|
return &p |
||||||
|
} |
||||||
|
|
||||||
|
// Float64SliceP is like Float64Slice, but accepts a shorthand letter that can be used after a single dash.
|
||||||
|
func (f *FlagSet) Float64SliceP(name, shorthand string, value []float64, usage string) *[]float64 { |
||||||
|
p := []float64{} |
||||||
|
f.Float64SliceVarP(&p, name, shorthand, value, usage) |
||||||
|
return &p |
||||||
|
} |
||||||
|
|
||||||
|
// Float64Slice defines a []float64 flag with specified name, default value, and usage string.
|
||||||
|
// The return value is the address of a []float64 variable that stores the value of the flag.
|
||||||
|
func Float64Slice(name string, value []float64, usage string) *[]float64 { |
||||||
|
return CommandLine.Float64SliceP(name, "", value, usage) |
||||||
|
} |
||||||
|
|
||||||
|
// Float64SliceP is like Float64Slice, but accepts a shorthand letter that can be used after a single dash.
|
||||||
|
func Float64SliceP(name, shorthand string, value []float64, usage string) *[]float64 { |
||||||
|
return CommandLine.Float64SliceP(name, shorthand, value, usage) |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
module github.com/spf13/pflag |
||||||
|
|
||||||
|
go 1.12 |
@ -0,0 +1,174 @@ |
|||||||
|
package pflag |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
// -- int32Slice Value
|
||||||
|
type int32SliceValue struct { |
||||||
|
value *[]int32 |
||||||
|
changed bool |
||||||
|
} |
||||||
|
|
||||||
|
func newInt32SliceValue(val []int32, p *[]int32) *int32SliceValue { |
||||||
|
isv := new(int32SliceValue) |
||||||
|
isv.value = p |
||||||
|
*isv.value = val |
||||||
|
return isv |
||||||
|
} |
||||||
|
|
||||||
|
func (s *int32SliceValue) Set(val string) error { |
||||||
|
ss := strings.Split(val, ",") |
||||||
|
out := make([]int32, len(ss)) |
||||||
|
for i, d := range ss { |
||||||
|
var err error |
||||||
|
var temp64 int64 |
||||||
|
temp64, err = strconv.ParseInt(d, 0, 32) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
out[i] = int32(temp64) |
||||||
|
|
||||||
|
} |
||||||
|
if !s.changed { |
||||||
|
*s.value = out |
||||||
|
} else { |
||||||
|
*s.value = append(*s.value, out...) |
||||||
|
} |
||||||
|
s.changed = true |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *int32SliceValue) Type() string { |
||||||
|
return "int32Slice" |
||||||
|
} |
||||||
|
|
||||||
|
func (s *int32SliceValue) String() string { |
||||||
|
out := make([]string, len(*s.value)) |
||||||
|
for i, d := range *s.value { |
||||||
|
out[i] = fmt.Sprintf("%d", d) |
||||||
|
} |
||||||
|
return "[" + strings.Join(out, ",") + "]" |
||||||
|
} |
||||||
|
|
||||||
|
func (s *int32SliceValue) fromString(val string) (int32, error) { |
||||||
|
t64, err := strconv.ParseInt(val, 0, 32) |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
return int32(t64), nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *int32SliceValue) toString(val int32) string { |
||||||
|
return fmt.Sprintf("%d", val) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *int32SliceValue) Append(val string) error { |
||||||
|
i, err := s.fromString(val) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
*s.value = append(*s.value, i) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *int32SliceValue) Replace(val []string) error { |
||||||
|
out := make([]int32, len(val)) |
||||||
|
for i, d := range val { |
||||||
|
var err error |
||||||
|
out[i], err = s.fromString(d) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
*s.value = out |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *int32SliceValue) GetSlice() []string { |
||||||
|
out := make([]string, len(*s.value)) |
||||||
|
for i, d := range *s.value { |
||||||
|
out[i] = s.toString(d) |
||||||
|
} |
||||||
|
return out |
||||||
|
} |
||||||
|
|
||||||
|
func int32SliceConv(val string) (interface{}, error) { |
||||||
|
val = strings.Trim(val, "[]") |
||||||
|
// Empty string would cause a slice with one (empty) entry
|
||||||
|
if len(val) == 0 { |
||||||
|
return []int32{}, nil |
||||||
|
} |
||||||
|
ss := strings.Split(val, ",") |
||||||
|
out := make([]int32, len(ss)) |
||||||
|
for i, d := range ss { |
||||||
|
var err error |
||||||
|
var temp64 int64 |
||||||
|
temp64, err = strconv.ParseInt(d, 0, 32) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
out[i] = int32(temp64) |
||||||
|
|
||||||
|
} |
||||||
|
return out, nil |
||||||
|
} |
||||||
|
|
||||||
|
// GetInt32Slice return the []int32 value of a flag with the given name
|
||||||
|
func (f *FlagSet) GetInt32Slice(name string) ([]int32, error) { |
||||||
|
val, err := f.getFlagType(name, "int32Slice", int32SliceConv) |
||||||
|
if err != nil { |
||||||
|
return []int32{}, err |
||||||
|
} |
||||||
|
return val.([]int32), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Int32SliceVar defines a int32Slice flag with specified name, default value, and usage string.
|
||||||
|
// The argument p points to a []int32 variable in which to store the value of the flag.
|
||||||
|
func (f *FlagSet) Int32SliceVar(p *[]int32, name string, value []int32, usage string) { |
||||||
|
f.VarP(newInt32SliceValue(value, p), name, "", usage) |
||||||
|
} |
||||||
|
|
||||||
|
// Int32SliceVarP is like Int32SliceVar, but accepts a shorthand letter that can be used after a single dash.
|
||||||
|
func (f *FlagSet) Int32SliceVarP(p *[]int32, name, shorthand string, value []int32, usage string) { |
||||||
|
f.VarP(newInt32SliceValue(value, p), name, shorthand, usage) |
||||||
|
} |
||||||
|
|
||||||
|
// Int32SliceVar defines a int32[] flag with specified name, default value, and usage string.
|
||||||
|
// The argument p points to a int32[] variable in which to store the value of the flag.
|
||||||
|
func Int32SliceVar(p *[]int32, name string, value []int32, usage string) { |
||||||
|
CommandLine.VarP(newInt32SliceValue(value, p), name, "", usage) |
||||||
|
} |
||||||
|
|
||||||
|
// Int32SliceVarP is like Int32SliceVar, but accepts a shorthand letter that can be used after a single dash.
|
||||||
|
func Int32SliceVarP(p *[]int32, name, shorthand string, value []int32, usage string) { |
||||||
|
CommandLine.VarP(newInt32SliceValue(value, p), name, shorthand, usage) |
||||||
|
} |
||||||
|
|
||||||
|
// Int32Slice defines a []int32 flag with specified name, default value, and usage string.
|
||||||
|
// The return value is the address of a []int32 variable that stores the value of the flag.
|
||||||
|
func (f *FlagSet) Int32Slice(name string, value []int32, usage string) *[]int32 { |
||||||
|
p := []int32{} |
||||||
|
f.Int32SliceVarP(&p, name, "", value, usage) |
||||||
|
return &p |
||||||
|
} |
||||||
|
|
||||||
|
// Int32SliceP is like Int32Slice, but accepts a shorthand letter that can be used after a single dash.
|
||||||
|
func (f *FlagSet) Int32SliceP(name, shorthand string, value []int32, usage string) *[]int32 { |
||||||
|
p := []int32{} |
||||||
|
f.Int32SliceVarP(&p, name, shorthand, value, usage) |
||||||
|
return &p |
||||||
|
} |
||||||
|
|
||||||
|
// Int32Slice defines a []int32 flag with specified name, default value, and usage string.
|
||||||
|
// The return value is the address of a []int32 variable that stores the value of the flag.
|
||||||
|
func Int32Slice(name string, value []int32, usage string) *[]int32 { |
||||||
|
return CommandLine.Int32SliceP(name, "", value, usage) |
||||||
|
} |
||||||
|
|
||||||
|
// Int32SliceP is like Int32Slice, but accepts a shorthand letter that can be used after a single dash.
|
||||||
|
func Int32SliceP(name, shorthand string, value []int32, usage string) *[]int32 { |
||||||
|
return CommandLine.Int32SliceP(name, shorthand, value, usage) |
||||||
|
} |
@ -0,0 +1,166 @@ |
|||||||
|
package pflag |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
// -- int64Slice Value
|
||||||
|
type int64SliceValue struct { |
||||||
|
value *[]int64 |
||||||
|
changed bool |
||||||
|
} |
||||||
|
|
||||||
|
func newInt64SliceValue(val []int64, p *[]int64) *int64SliceValue { |
||||||
|
isv := new(int64SliceValue) |
||||||
|
isv.value = p |
||||||
|
*isv.value = val |
||||||
|
return isv |
||||||
|
} |
||||||
|
|
||||||
|
func (s *int64SliceValue) Set(val string) error { |
||||||
|
ss := strings.Split(val, ",") |
||||||
|
out := make([]int64, len(ss)) |
||||||
|
for i, d := range ss { |
||||||
|
var err error |
||||||
|
out[i], err = strconv.ParseInt(d, 0, 64) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
if !s.changed { |
||||||
|
*s.value = out |
||||||
|
} else { |
||||||
|
*s.value = append(*s.value, out...) |
||||||
|
} |
||||||
|
s.changed = true |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *int64SliceValue) Type() string { |
||||||
|
return "int64Slice" |
||||||
|
} |
||||||
|
|
||||||
|
func (s *int64SliceValue) String() string { |
||||||
|
out := make([]string, len(*s.value)) |
||||||
|
for i, d := range *s.value { |
||||||
|
out[i] = fmt.Sprintf("%d", d) |
||||||
|
} |
||||||
|
return "[" + strings.Join(out, ",") + "]" |
||||||
|
} |
||||||
|
|
||||||
|
func (s *int64SliceValue) fromString(val string) (int64, error) { |
||||||
|
return strconv.ParseInt(val, 0, 64) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *int64SliceValue) toString(val int64) string { |
||||||
|
return fmt.Sprintf("%d", val) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *int64SliceValue) Append(val string) error { |
||||||
|
i, err := s.fromString(val) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
*s.value = append(*s.value, i) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *int64SliceValue) Replace(val []string) error { |
||||||
|
out := make([]int64, len(val)) |
||||||
|
for i, d := range val { |
||||||
|
var err error |
||||||
|
out[i], err = s.fromString(d) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
*s.value = out |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *int64SliceValue) GetSlice() []string { |
||||||
|
out := make([]string, len(*s.value)) |
||||||
|
for i, d := range *s.value { |
||||||
|
out[i] = s.toString(d) |
||||||
|
} |
||||||
|
return out |
||||||
|
} |
||||||
|
|
||||||
|
func int64SliceConv(val string) (interface{}, error) { |
||||||
|
val = strings.Trim(val, "[]") |
||||||
|
// Empty string would cause a slice with one (empty) entry
|
||||||
|
if len(val) == 0 { |
||||||
|
return []int64{}, nil |
||||||
|
} |
||||||
|
ss := strings.Split(val, ",") |
||||||
|
out := make([]int64, len(ss)) |
||||||
|
for i, d := range ss { |
||||||
|
var err error |
||||||
|
out[i], err = strconv.ParseInt(d, 0, 64) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
return out, nil |
||||||
|
} |
||||||
|
|
||||||
|
// GetInt64Slice return the []int64 value of a flag with the given name
|
||||||
|
func (f *FlagSet) GetInt64Slice(name string) ([]int64, error) { |
||||||
|
val, err := f.getFlagType(name, "int64Slice", int64SliceConv) |
||||||
|
if err != nil { |
||||||
|
return []int64{}, err |
||||||
|
} |
||||||
|
return val.([]int64), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Int64SliceVar defines a int64Slice flag with specified name, default value, and usage string.
|
||||||
|
// The argument p points to a []int64 variable in which to store the value of the flag.
|
||||||
|
func (f *FlagSet) Int64SliceVar(p *[]int64, name string, value []int64, usage string) { |
||||||
|
f.VarP(newInt64SliceValue(value, p), name, "", usage) |
||||||
|
} |
||||||
|
|
||||||
|
// Int64SliceVarP is like Int64SliceVar, but accepts a shorthand letter that can be used after a single dash.
|
||||||
|
func (f *FlagSet) Int64SliceVarP(p *[]int64, name, shorthand string, value []int64, usage string) { |
||||||
|
f.VarP(newInt64SliceValue(value, p), name, shorthand, usage) |
||||||
|
} |
||||||
|
|
||||||
|
// Int64SliceVar defines a int64[] flag with specified name, default value, and usage string.
|
||||||
|
// The argument p points to a int64[] variable in which to store the value of the flag.
|
||||||
|
func Int64SliceVar(p *[]int64, name string, value []int64, usage string) { |
||||||
|
CommandLine.VarP(newInt64SliceValue(value, p), name, "", usage) |
||||||
|
} |
||||||
|
|
||||||
|
// Int64SliceVarP is like Int64SliceVar, but accepts a shorthand letter that can be used after a single dash.
|
||||||
|
func Int64SliceVarP(p *[]int64, name, shorthand string, value []int64, usage string) { |
||||||
|
CommandLine.VarP(newInt64SliceValue(value, p), name, shorthand, usage) |
||||||
|
} |
||||||
|
|
||||||
|
// Int64Slice defines a []int64 flag with specified name, default value, and usage string.
|
||||||
|
// The return value is the address of a []int64 variable that stores the value of the flag.
|
||||||
|
func (f *FlagSet) Int64Slice(name string, value []int64, usage string) *[]int64 { |
||||||
|
p := []int64{} |
||||||
|
f.Int64SliceVarP(&p, name, "", value, usage) |
||||||
|
return &p |
||||||
|
} |
||||||
|
|
||||||
|
// Int64SliceP is like Int64Slice, but accepts a shorthand letter that can be used after a single dash.
|
||||||
|
func (f *FlagSet) Int64SliceP(name, shorthand string, value []int64, usage string) *[]int64 { |
||||||
|
p := []int64{} |
||||||
|
f.Int64SliceVarP(&p, name, shorthand, value, usage) |
||||||
|
return &p |
||||||
|
} |
||||||
|
|
||||||
|
// Int64Slice defines a []int64 flag with specified name, default value, and usage string.
|
||||||
|
// The return value is the address of a []int64 variable that stores the value of the flag.
|
||||||
|
func Int64Slice(name string, value []int64, usage string) *[]int64 { |
||||||
|
return CommandLine.Int64SliceP(name, "", value, usage) |
||||||
|
} |
||||||
|
|
||||||
|
// Int64SliceP is like Int64Slice, but accepts a shorthand letter that can be used after a single dash.
|
||||||
|
func Int64SliceP(name, shorthand string, value []int64, usage string) *[]int64 { |
||||||
|
return CommandLine.Int64SliceP(name, shorthand, value, usage) |
||||||
|
} |
@ -0,0 +1,149 @@ |
|||||||
|
package pflag |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"fmt" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
// -- stringToInt64 Value
|
||||||
|
type stringToInt64Value struct { |
||||||
|
value *map[string]int64 |
||||||
|
changed bool |
||||||
|
} |
||||||
|
|
||||||
|
func newStringToInt64Value(val map[string]int64, p *map[string]int64) *stringToInt64Value { |
||||||
|
ssv := new(stringToInt64Value) |
||||||
|
ssv.value = p |
||||||
|
*ssv.value = val |
||||||
|
return ssv |
||||||
|
} |
||||||
|
|
||||||
|
// Format: a=1,b=2
|
||||||
|
func (s *stringToInt64Value) Set(val string) error { |
||||||
|
ss := strings.Split(val, ",") |
||||||
|
out := make(map[string]int64, len(ss)) |
||||||
|
for _, pair := range ss { |
||||||
|
kv := strings.SplitN(pair, "=", 2) |
||||||
|
if len(kv) != 2 { |
||||||
|
return fmt.Errorf("%s must be formatted as key=value", pair) |
||||||
|
} |
||||||
|
var err error |
||||||
|
out[kv[0]], err = strconv.ParseInt(kv[1], 10, 64) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
if !s.changed { |
||||||
|
*s.value = out |
||||||
|
} else { |
||||||
|
for k, v := range out { |
||||||
|
(*s.value)[k] = v |
||||||
|
} |
||||||
|
} |
||||||
|
s.changed = true |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *stringToInt64Value) Type() string { |
||||||
|
return "stringToInt64" |
||||||
|
} |
||||||
|
|
||||||
|
func (s *stringToInt64Value) String() string { |
||||||
|
var buf bytes.Buffer |
||||||
|
i := 0 |
||||||
|
for k, v := range *s.value { |
||||||
|
if i > 0 { |
||||||
|
buf.WriteRune(',') |
||||||
|
} |
||||||
|
buf.WriteString(k) |
||||||
|
buf.WriteRune('=') |
||||||
|
buf.WriteString(strconv.FormatInt(v, 10)) |
||||||
|
i++ |
||||||
|
} |
||||||
|
return "[" + buf.String() + "]" |
||||||
|
} |
||||||
|
|
||||||
|
func stringToInt64Conv(val string) (interface{}, error) { |
||||||
|
val = strings.Trim(val, "[]") |
||||||
|
// An empty string would cause an empty map
|
||||||
|
if len(val) == 0 { |
||||||
|
return map[string]int64{}, nil |
||||||
|
} |
||||||
|
ss := strings.Split(val, ",") |
||||||
|
out := make(map[string]int64, len(ss)) |
||||||
|
for _, pair := range ss { |
||||||
|
kv := strings.SplitN(pair, "=", 2) |
||||||
|
if len(kv) != 2 { |
||||||
|
return nil, fmt.Errorf("%s must be formatted as key=value", pair) |
||||||
|
} |
||||||
|
var err error |
||||||
|
out[kv[0]], err = strconv.ParseInt(kv[1], 10, 64) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
return out, nil |
||||||
|
} |
||||||
|
|
||||||
|
// GetStringToInt64 return the map[string]int64 value of a flag with the given name
|
||||||
|
func (f *FlagSet) GetStringToInt64(name string) (map[string]int64, error) { |
||||||
|
val, err := f.getFlagType(name, "stringToInt64", stringToInt64Conv) |
||||||
|
if err != nil { |
||||||
|
return map[string]int64{}, err |
||||||
|
} |
||||||
|
return val.(map[string]int64), nil |
||||||
|
} |
||||||
|
|
||||||
|
// StringToInt64Var defines a string flag with specified name, default value, and usage string.
|
||||||
|
// The argument p point64s to a map[string]int64 variable in which to store the values of the multiple flags.
|
||||||
|
// The value of each argument will not try to be separated by comma
|
||||||
|
func (f *FlagSet) StringToInt64Var(p *map[string]int64, name string, value map[string]int64, usage string) { |
||||||
|
f.VarP(newStringToInt64Value(value, p), name, "", usage) |
||||||
|
} |
||||||
|
|
||||||
|
// StringToInt64VarP is like StringToInt64Var, but accepts a shorthand letter that can be used after a single dash.
|
||||||
|
func (f *FlagSet) StringToInt64VarP(p *map[string]int64, name, shorthand string, value map[string]int64, usage string) { |
||||||
|
f.VarP(newStringToInt64Value(value, p), name, shorthand, usage) |
||||||
|
} |
||||||
|
|
||||||
|
// StringToInt64Var defines a string flag with specified name, default value, and usage string.
|
||||||
|
// The argument p point64s to a map[string]int64 variable in which to store the value of the flag.
|
||||||
|
// The value of each argument will not try to be separated by comma
|
||||||
|
func StringToInt64Var(p *map[string]int64, name string, value map[string]int64, usage string) { |
||||||
|
CommandLine.VarP(newStringToInt64Value(value, p), name, "", usage) |
||||||
|
} |
||||||
|
|
||||||
|
// StringToInt64VarP is like StringToInt64Var, but accepts a shorthand letter that can be used after a single dash.
|
||||||
|
func StringToInt64VarP(p *map[string]int64, name, shorthand string, value map[string]int64, usage string) { |
||||||
|
CommandLine.VarP(newStringToInt64Value(value, p), name, shorthand, usage) |
||||||
|
} |
||||||
|
|
||||||
|
// StringToInt64 defines a string flag with specified name, default value, and usage string.
|
||||||
|
// The return value is the address of a map[string]int64 variable that stores the value of the flag.
|
||||||
|
// The value of each argument will not try to be separated by comma
|
||||||
|
func (f *FlagSet) StringToInt64(name string, value map[string]int64, usage string) *map[string]int64 { |
||||||
|
p := map[string]int64{} |
||||||
|
f.StringToInt64VarP(&p, name, "", value, usage) |
||||||
|
return &p |
||||||
|
} |
||||||
|
|
||||||
|
// StringToInt64P is like StringToInt64, but accepts a shorthand letter that can be used after a single dash.
|
||||||
|
func (f *FlagSet) StringToInt64P(name, shorthand string, value map[string]int64, usage string) *map[string]int64 { |
||||||
|
p := map[string]int64{} |
||||||
|
f.StringToInt64VarP(&p, name, shorthand, value, usage) |
||||||
|
return &p |
||||||
|
} |
||||||
|
|
||||||
|
// StringToInt64 defines a string flag with specified name, default value, and usage string.
|
||||||
|
// The return value is the address of a map[string]int64 variable that stores the value of the flag.
|
||||||
|
// The value of each argument will not try to be separated by comma
|
||||||
|
func StringToInt64(name string, value map[string]int64, usage string) *map[string]int64 { |
||||||
|
return CommandLine.StringToInt64P(name, "", value, usage) |
||||||
|
} |
||||||
|
|
||||||
|
// StringToInt64P is like StringToInt64, but accepts a shorthand letter that can be used after a single dash.
|
||||||
|
func StringToInt64P(name, shorthand string, value map[string]int64, usage string) *map[string]int64 { |
||||||
|
return CommandLine.StringToInt64P(name, shorthand, value, usage) |
||||||
|
} |
@ -1,26 +0,0 @@ |
|||||||
language: go |
|
||||||
|
|
||||||
go: |
|
||||||
- '1.9' |
|
||||||
- '1.10' |
|
||||||
|
|
||||||
services: |
|
||||||
- postgresql |
|
||||||
- mysql |
|
||||||
|
|
||||||
addons: |
|
||||||
postgresql: "9.4" |
|
||||||
|
|
||||||
before_script: |
|
||||||
- mysql -e 'CREATE DATABASE testfixtures_test;' |
|
||||||
- psql -c 'CREATE DATABASE testfixtures_test;' -U postgres |
|
||||||
|
|
||||||
install: |
|
||||||
- go get -t -tags 'sqlite postgresql mysql' ./... |
|
||||||
- curl -s https://raw.githubusercontent.com/go-task/task/master/install-task.sh | sh |
|
||||||
- bin/task dl-deps |
|
||||||
- cp .sample.env .env |
|
||||||
|
|
||||||
script: |
|
||||||
- bin/task lint |
|
||||||
- bin/task test-free |
|
@ -1,64 +0,0 @@ |
|||||||
# github.com/go-task/task |
|
||||||
|
|
||||||
version: '2' |
|
||||||
|
|
||||||
tasks: |
|
||||||
dl-deps: |
|
||||||
desc: Download cli deps |
|
||||||
cmds: |
|
||||||
- go get -u github.com/golang/lint/golint |
|
||||||
|
|
||||||
lint: |
|
||||||
desc: Runs golint |
|
||||||
cmds: |
|
||||||
- golint . |
|
||||||
|
|
||||||
test-free: |
|
||||||
desc: Test free databases (PG, MySQL and SQLite) |
|
||||||
cmds: |
|
||||||
- task: test-pg |
|
||||||
- task: test-mysql |
|
||||||
- task: test-sqlite |
|
||||||
|
|
||||||
test-all: |
|
||||||
desc: Test all databases (PG, MySQL, SQLite, SQLServer and Oracle) |
|
||||||
cmds: |
|
||||||
- task: test-pg |
|
||||||
- task: test-mysql |
|
||||||
- task: test-sqlite |
|
||||||
- task: test-sqlserver |
|
||||||
- task: test-oracle |
|
||||||
|
|
||||||
test-pg: |
|
||||||
desc: Test PostgreSQL |
|
||||||
cmds: |
|
||||||
- task: test-db |
|
||||||
vars: {DATABASE: postgresql} |
|
||||||
|
|
||||||
test-mysql: |
|
||||||
desc: Test MySQL |
|
||||||
cmds: |
|
||||||
- task: test-db |
|
||||||
vars: {DATABASE: mysql} |
|
||||||
|
|
||||||
test-sqlite: |
|
||||||
desc: Test SQLite |
|
||||||
cmds: |
|
||||||
- task: test-db |
|
||||||
vars: {DATABASE: sqlite} |
|
||||||
|
|
||||||
test-sqlserver: |
|
||||||
desc: Test SQLServer |
|
||||||
cmds: |
|
||||||
- task: test-db |
|
||||||
vars: {DATABASE: sqlserver} |
|
||||||
|
|
||||||
test-oracle: |
|
||||||
desc: Test Oracle |
|
||||||
cmds: |
|
||||||
- task: test-db |
|
||||||
vars: {DATABASE: oracle} |
|
||||||
|
|
||||||
test-db: |
|
||||||
cmds: |
|
||||||
- go test -v -tags {{.DATABASE}} |
|
@ -1,51 +0,0 @@ |
|||||||
version: '{build}' |
|
||||||
|
|
||||||
clone_folder: C:\GOPATH\src\gopkg.in\testfixtures.v2 |
|
||||||
|
|
||||||
build: false |
|
||||||
deploy: false |
|
||||||
|
|
||||||
services: |
|
||||||
- postgresql96 |
|
||||||
- mysql |
|
||||||
- mssql2017 |
|
||||||
|
|
||||||
environment: |
|
||||||
POSTGRES_PATH: C:\Program Files\PostgreSQL\9.6 |
|
||||||
PGUSER: postgres |
|
||||||
PGPASSWORD: Password12! |
|
||||||
PG_CONN_STRING: 'user=postgres password=Password12! dbname=testfixtures_test sslmode=disable' |
|
||||||
|
|
||||||
MYSQL_PATH: C:\Program Files\MySql\MySQL Server 5.7 |
|
||||||
MYSQL_PWD: Password12! |
|
||||||
MYSQL_CONN_STRING: 'root:Password12!@/testfixtures_test?multiStatements=true' |
|
||||||
|
|
||||||
SQLITE_CONN_STRING: 'testdb.sqlite3' |
|
||||||
|
|
||||||
SQLSERVER_CONN_STRING: 'server=localhost;database=testfixtures_test;user id=sa;password=Password12!;encrypt=disable' |
|
||||||
|
|
||||||
MINGW_PATH: C:\MinGW |
|
||||||
|
|
||||||
GOPATH: C:\GOPATH |
|
||||||
GOVERSION: 1.10.3 |
|
||||||
|
|
||||||
install: |
|
||||||
- SET PATH=%POSTGRES_PATH%\bin;%MYSQL_PATH%\bin;%MINGW_PATH%\bin;%PATH% |
|
||||||
|
|
||||||
- rmdir C:\go /s /q |
|
||||||
- appveyor DownloadFile https://storage.googleapis.com/golang/go%GOVERSION%.windows-386.msi |
|
||||||
- msiexec /i go%GOVERSION%.windows-386.msi /q |
|
||||||
- go version |
|
||||||
|
|
||||||
build_script: |
|
||||||
- createdb testfixtures_test |
|
||||||
- mysql -e "CREATE DATABASE testfixtures_test;" --user=root |
|
||||||
- sqlcmd -S localhost,1433 -U sa -P Password12! -Q "CREATE DATABASE testfixtures_test" -d "master" |
|
||||||
|
|
||||||
test_script: |
|
||||||
- go get -t -tags "sqlite postgresql mysql sqlserver" ./... |
|
||||||
- go install -v ./... |
|
||||||
- go test -v -tags postgresql |
|
||||||
- go test -v -tags mysql |
|
||||||
- go test -v -tags sqlserver |
|
||||||
- go test -v -tags sqlite |
|
@ -1,75 +0,0 @@ |
|||||||
package testfixtures |
|
||||||
|
|
||||||
import ( |
|
||||||
"database/sql" |
|
||||||
) |
|
||||||
|
|
||||||
type ( |
|
||||||
// DataBaseHelper is the helper interface
|
|
||||||
// Deprecated: Use Helper instead
|
|
||||||
DataBaseHelper Helper |
|
||||||
|
|
||||||
// PostgreSQLHelper is the PostgreSQL helper
|
|
||||||
// Deprecated: Use PostgreSQL{} instead
|
|
||||||
PostgreSQLHelper struct { |
|
||||||
PostgreSQL |
|
||||||
UseAlterConstraint bool |
|
||||||
} |
|
||||||
|
|
||||||
// MySQLHelper is the MySQL helper
|
|
||||||
// Deprecated: Use MySQL{} instead
|
|
||||||
MySQLHelper struct { |
|
||||||
MySQL |
|
||||||
} |
|
||||||
|
|
||||||
// SQLiteHelper is the SQLite helper
|
|
||||||
// Deprecated: Use SQLite{} instead
|
|
||||||
SQLiteHelper struct { |
|
||||||
SQLite |
|
||||||
} |
|
||||||
|
|
||||||
// SQLServerHelper is the SQLServer helper
|
|
||||||
// Deprecated: Use SQLServer{} instead
|
|
||||||
SQLServerHelper struct { |
|
||||||
SQLServer |
|
||||||
} |
|
||||||
|
|
||||||
// OracleHelper is the Oracle helper
|
|
||||||
// Deprecated: Use Oracle{} instead
|
|
||||||
OracleHelper struct { |
|
||||||
Oracle |
|
||||||
} |
|
||||||
) |
|
||||||
|
|
||||||
func (h *PostgreSQLHelper) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) error { |
|
||||||
h.PostgreSQL.UseAlterConstraint = h.UseAlterConstraint |
|
||||||
return h.PostgreSQL.disableReferentialIntegrity(db, loadFn) |
|
||||||
} |
|
||||||
|
|
||||||
// LoadFixtureFiles load all specified fixtures files into database:
|
|
||||||
// LoadFixtureFiles(db, &PostgreSQL{},
|
|
||||||
// "fixtures/customers.yml", "fixtures/orders.yml")
|
|
||||||
// // add as many files you want
|
|
||||||
//
|
|
||||||
// Deprecated: Use NewFiles() and Load() instead.
|
|
||||||
func LoadFixtureFiles(db *sql.DB, helper Helper, files ...string) error { |
|
||||||
c, err := NewFiles(db, helper, files...) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
return c.Load() |
|
||||||
} |
|
||||||
|
|
||||||
// LoadFixtures loads all fixtures in a given folder into the database:
|
|
||||||
// LoadFixtures("myfixturesfolder", db, &PostgreSQL{})
|
|
||||||
//
|
|
||||||
// Deprecated: Use NewFolder() and Load() instead.
|
|
||||||
func LoadFixtures(folderName string, db *sql.DB, helper Helper) error { |
|
||||||
c, err := NewFolder(db, helper, folderName) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
return c.Load() |
|
||||||
} |
|
@ -1,41 +0,0 @@ |
|||||||
package testfixtures |
|
||||||
|
|
||||||
import ( |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
) |
|
||||||
|
|
||||||
var ( |
|
||||||
// ErrWrongCastNotAMap is returned when a map is not a map[interface{}]interface{}
|
|
||||||
ErrWrongCastNotAMap = errors.New("Could not cast record: not a map[interface{}]interface{}") |
|
||||||
|
|
||||||
// ErrFileIsNotSliceOrMap is returned the the fixture file is not a slice or map.
|
|
||||||
ErrFileIsNotSliceOrMap = errors.New("The fixture file is not a slice or map") |
|
||||||
|
|
||||||
// ErrKeyIsNotString is returned when a record is not of type string
|
|
||||||
ErrKeyIsNotString = errors.New("Record map key is not string") |
|
||||||
|
|
||||||
// ErrNotTestDatabase is returned when the database name doesn't contains "test"
|
|
||||||
ErrNotTestDatabase = errors.New(`Loading aborted because the database name does not contains "test"`) |
|
||||||
) |
|
||||||
|
|
||||||
// InsertError will be returned if any error happens on database while
|
|
||||||
// inserting the record
|
|
||||||
type InsertError struct { |
|
||||||
Err error |
|
||||||
File string |
|
||||||
Index int |
|
||||||
SQL string |
|
||||||
Params []interface{} |
|
||||||
} |
|
||||||
|
|
||||||
func (e *InsertError) Error() string { |
|
||||||
return fmt.Sprintf( |
|
||||||
"testfixtures: error inserting record: %v, on file: %s, index: %d, sql: %s, params: %v", |
|
||||||
e.Err, |
|
||||||
e.File, |
|
||||||
e.Index, |
|
||||||
e.SQL, |
|
||||||
e.Params, |
|
||||||
) |
|
||||||
} |
|
@ -1,110 +0,0 @@ |
|||||||
package testfixtures |
|
||||||
|
|
||||||
import ( |
|
||||||
"database/sql" |
|
||||||
"fmt" |
|
||||||
"os" |
|
||||||
"path" |
|
||||||
"unicode/utf8" |
|
||||||
|
|
||||||
"gopkg.in/yaml.v2" |
|
||||||
) |
|
||||||
|
|
||||||
// TableInfo is settings for generating a fixture for table.
|
|
||||||
type TableInfo struct { |
|
||||||
Name string // Table name
|
|
||||||
Where string // A condition for extracting records. If this value is empty, extracts all records.
|
|
||||||
} |
|
||||||
|
|
||||||
func (ti *TableInfo) whereClause() string { |
|
||||||
if ti.Where == "" { |
|
||||||
return "" |
|
||||||
} |
|
||||||
return fmt.Sprintf(" WHERE %s", ti.Where) |
|
||||||
} |
|
||||||
|
|
||||||
// GenerateFixtures generates fixtures for the current contents of a database, and saves
|
|
||||||
// them to the specified directory
|
|
||||||
func GenerateFixtures(db *sql.DB, helper Helper, dir string) error { |
|
||||||
tables, err := helper.tableNames(db) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
for _, table := range tables { |
|
||||||
filename := path.Join(dir, table+".yml") |
|
||||||
if err := generateFixturesForTable(db, helper, &TableInfo{Name: table}, filename); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
// GenerateFixturesForTables generates fixtures for the current contents of specified tables in a database, and saves
|
|
||||||
// them to the specified directory
|
|
||||||
func GenerateFixturesForTables(db *sql.DB, tables []*TableInfo, helper Helper, dir string) error { |
|
||||||
for _, table := range tables { |
|
||||||
filename := path.Join(dir, table.Name+".yml") |
|
||||||
if err := generateFixturesForTable(db, helper, table, filename); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func generateFixturesForTable(db *sql.DB, h Helper, table *TableInfo, filename string) error { |
|
||||||
query := fmt.Sprintf("SELECT * FROM %s%s", h.quoteKeyword(table.Name), table.whereClause()) |
|
||||||
rows, err := db.Query(query) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
defer rows.Close() |
|
||||||
|
|
||||||
columns, err := rows.Columns() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
fixtures := make([]interface{}, 0, 10) |
|
||||||
for rows.Next() { |
|
||||||
entries := make([]interface{}, len(columns)) |
|
||||||
entryPtrs := make([]interface{}, len(entries)) |
|
||||||
for i := range entries { |
|
||||||
entryPtrs[i] = &entries[i] |
|
||||||
} |
|
||||||
if err := rows.Scan(entryPtrs...); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
entryMap := make(map[string]interface{}, len(entries)) |
|
||||||
for i, column := range columns { |
|
||||||
entryMap[column] = convertValue(entries[i]) |
|
||||||
} |
|
||||||
fixtures = append(fixtures, entryMap) |
|
||||||
} |
|
||||||
if err = rows.Err(); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
f, err := os.Create(filename) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
defer f.Close() |
|
||||||
|
|
||||||
marshaled, err := yaml.Marshal(fixtures) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
_, err = f.Write(marshaled) |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
func convertValue(value interface{}) interface{} { |
|
||||||
switch v := value.(type) { |
|
||||||
case []byte: |
|
||||||
if utf8.Valid(v) { |
|
||||||
return string(v) |
|
||||||
} |
|
||||||
} |
|
||||||
return value |
|
||||||
} |
|
@ -1,19 +0,0 @@ |
|||||||
package testfixtures |
|
||||||
|
|
||||||
var ( |
|
||||||
skipDatabaseNameCheck bool |
|
||||||
resetSequencesTo int64 = 10000 |
|
||||||
) |
|
||||||
|
|
||||||
// SkipDatabaseNameCheck If true, loading fixtures will not check if the database
|
|
||||||
// name constaint "test". Use with caution!
|
|
||||||
func SkipDatabaseNameCheck(value bool) { |
|
||||||
skipDatabaseNameCheck = value |
|
||||||
} |
|
||||||
|
|
||||||
// ResetSequencesTo sets the value the sequences will be reset to.
|
|
||||||
// This is used by PostgreSQL and Oracle.
|
|
||||||
// Defaults to 10000.
|
|
||||||
func ResetSequencesTo(value int64) { |
|
||||||
resetSequencesTo = value |
|
||||||
} |
|
@ -1,171 +0,0 @@ |
|||||||
package testfixtures |
|
||||||
|
|
||||||
import ( |
|
||||||
"database/sql" |
|
||||||
"fmt" |
|
||||||
"strings" |
|
||||||
) |
|
||||||
|
|
||||||
// Oracle is the Oracle database helper for this package
|
|
||||||
type Oracle struct { |
|
||||||
baseHelper |
|
||||||
|
|
||||||
enabledConstraints []oracleConstraint |
|
||||||
sequences []string |
|
||||||
} |
|
||||||
|
|
||||||
type oracleConstraint struct { |
|
||||||
tableName string |
|
||||||
constraintName string |
|
||||||
} |
|
||||||
|
|
||||||
func (h *Oracle) init(db *sql.DB) error { |
|
||||||
var err error |
|
||||||
|
|
||||||
h.enabledConstraints, err = h.getEnabledConstraints(db) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
h.sequences, err = h.getSequences(db) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (*Oracle) paramType() int { |
|
||||||
return paramTypeColon |
|
||||||
} |
|
||||||
|
|
||||||
func (*Oracle) quoteKeyword(str string) string { |
|
||||||
return fmt.Sprintf("\"%s\"", strings.ToUpper(str)) |
|
||||||
} |
|
||||||
|
|
||||||
func (*Oracle) databaseName(q queryable) (string, error) { |
|
||||||
var dbName string |
|
||||||
err := q.QueryRow("SELECT user FROM DUAL").Scan(&dbName) |
|
||||||
return dbName, err |
|
||||||
} |
|
||||||
|
|
||||||
func (*Oracle) tableNames(q queryable) ([]string, error) { |
|
||||||
query := ` |
|
||||||
SELECT TABLE_NAME |
|
||||||
FROM USER_TABLES |
|
||||||
` |
|
||||||
rows, err := q.Query(query) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
defer rows.Close() |
|
||||||
|
|
||||||
var tables []string |
|
||||||
for rows.Next() { |
|
||||||
var table string |
|
||||||
if err = rows.Scan(&table); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
tables = append(tables, table) |
|
||||||
} |
|
||||||
if err = rows.Err(); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return tables, nil |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
func (*Oracle) getEnabledConstraints(q queryable) ([]oracleConstraint, error) { |
|
||||||
var constraints []oracleConstraint |
|
||||||
rows, err := q.Query(` |
|
||||||
SELECT table_name, constraint_name |
|
||||||
FROM user_constraints |
|
||||||
WHERE constraint_type = 'R' |
|
||||||
AND status = 'ENABLED' |
|
||||||
`) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
defer rows.Close() |
|
||||||
|
|
||||||
for rows.Next() { |
|
||||||
var constraint oracleConstraint |
|
||||||
rows.Scan(&constraint.tableName, &constraint.constraintName) |
|
||||||
constraints = append(constraints, constraint) |
|
||||||
} |
|
||||||
if err = rows.Err(); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return constraints, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (*Oracle) getSequences(q queryable) ([]string, error) { |
|
||||||
var sequences []string |
|
||||||
rows, err := q.Query("SELECT sequence_name FROM user_sequences") |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
defer rows.Close() |
|
||||||
|
|
||||||
for rows.Next() { |
|
||||||
var sequence string |
|
||||||
if err = rows.Scan(&sequence); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
sequences = append(sequences, sequence) |
|
||||||
} |
|
||||||
if err = rows.Err(); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return sequences, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (h *Oracle) resetSequences(q queryable) error { |
|
||||||
for _, sequence := range h.sequences { |
|
||||||
_, err := q.Exec(fmt.Sprintf("DROP SEQUENCE %s", h.quoteKeyword(sequence))) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
_, err = q.Exec(fmt.Sprintf("CREATE SEQUENCE %s START WITH %d", h.quoteKeyword(sequence), resetSequencesTo)) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (h *Oracle) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) (err error) { |
|
||||||
// re-enable after load
|
|
||||||
defer func() { |
|
||||||
for _, c := range h.enabledConstraints { |
|
||||||
_, err2 := db.Exec(fmt.Sprintf("ALTER TABLE %s ENABLE CONSTRAINT %s", h.quoteKeyword(c.tableName), h.quoteKeyword(c.constraintName))) |
|
||||||
if err2 != nil && err == nil { |
|
||||||
err = err2 |
|
||||||
} |
|
||||||
} |
|
||||||
}() |
|
||||||
|
|
||||||
// disable foreign keys
|
|
||||||
for _, c := range h.enabledConstraints { |
|
||||||
_, err := db.Exec(fmt.Sprintf("ALTER TABLE %s DISABLE CONSTRAINT %s", h.quoteKeyword(c.tableName), h.quoteKeyword(c.constraintName))) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
tx, err := db.Begin() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
defer tx.Rollback() |
|
||||||
|
|
||||||
if err = loadFn(tx); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
if err = tx.Commit(); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
return h.resetSequences(db) |
|
||||||
} |
|
@ -1,305 +0,0 @@ |
|||||||
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") |
|
||||||
) |
|
||||||
|
|
||||||
// NewFolder creates a context for all fixtures in a given folder into the database:
|
|
||||||
// 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 |
|
||||||
} |
|
||||||
|
|
||||||
// NewFiles creates a context for all specified fixtures files into database:
|
|
||||||
// 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 |
|
||||||
} |
|
||||||
|
|
||||||
// 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 |
|
||||||
} |
|
||||||
|
|
||||||
// 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 { |
|
||||||
if err := c.DetectTestDatabase(); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
err := c.helper.disableReferentialIntegrity(c.db, func(tx *sql.Tx) error { |
|
||||||
for _, file := range c.fixturesFiles { |
|
||||||
modified, err := c.helper.isTableModified(tx, file.fileNameWithoutExtension()) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
if !modified { |
|
||||||
continue |
|
||||||
} |
|
||||||
if err := file.delete(tx, c.helper); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
err = c.helper.whileInsertOnTable(tx, file.fileNameWithoutExtension(), func() error { |
|
||||||
for j, i := range file.insertSQLs { |
|
||||||
if _, err := tx.Exec(i.sql, i.params...); err != nil { |
|
||||||
return &InsertError{ |
|
||||||
Err: err, |
|
||||||
File: file.fileName, |
|
||||||
Index: j, |
|
||||||
SQL: i.sql, |
|
||||||
Params: i.params, |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
return nil |
|
||||||
}) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
return nil |
|
||||||
}) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
return c.helper.afterLoad(c.db) |
|
||||||
} |
|
||||||
|
|
||||||
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)) |
|
||||||
|
|
||||||
// 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) |
|
||||||
} |
|
||||||
|
|
||||||
switch h.paramType() { |
|
||||||
case paramTypeDollar: |
|
||||||
sqlValues = append(sqlValues, fmt.Sprintf("$%d", i)) |
|
||||||
case paramTypeQuestion: |
|
||||||
sqlValues = append(sqlValues, "?") |
|
||||||
case paramTypeColon: |
|
||||||
sqlValues = append(sqlValues, fmt.Sprintf(":%d", i)) |
|
||||||
} |
|
||||||
|
|
||||||
values = append(values, value) |
|
||||||
i++ |
|
||||||
} |
|
||||||
|
|
||||||
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 |
|
||||||
} |
|
@ -1,34 +0,0 @@ |
|||||||
package testfixtures |
|
||||||
|
|
||||||
import ( |
|
||||||
"errors" |
|
||||||
"time" |
|
||||||
) |
|
||||||
|
|
||||||
var timeFormats = []string{ |
|
||||||
"2006-01-02", |
|
||||||
"2006-01-02 15:04", |
|
||||||
"2006-01-02 15:04:05", |
|
||||||
"20060102", |
|
||||||
"20060102 15:04", |
|
||||||
"20060102 15:04:05", |
|
||||||
"02/01/2006", |
|
||||||
"02/01/2006 15:04", |
|
||||||
"02/01/2006 15:04:05", |
|
||||||
"2006-01-02T15:04-07:00", |
|
||||||
"2006-01-02T15:04:05-07:00", |
|
||||||
} |
|
||||||
|
|
||||||
// ErrCouldNotConvertToTime is returns when a string is not a reconizable time format
|
|
||||||
var ErrCouldNotConvertToTime = errors.New("Could not convert string to time") |
|
||||||
|
|
||||||
func tryStrToDate(s string) (time.Time, error) { |
|
||||||
for _, f := range timeFormats { |
|
||||||
t, err := time.ParseInLocation(f, s, time.Local) |
|
||||||
if err != nil { |
|
||||||
continue |
|
||||||
} |
|
||||||
return t, nil |
|
||||||
} |
|
||||||
return time.Time{}, ErrCouldNotConvertToTime |
|
||||||
} |
|
Loading…
Reference in new issue