diff --git a/book.go b/book.go index 81a307b9..c2e8668f 100644 --- a/book.go +++ b/book.go @@ -22,6 +22,7 @@ type book struct { Vars map[string]interface{} `yaml:"vars,omitempty"` Steps []map[string]interface{} `yaml:"steps,omitempty"` Debug bool `yaml:"debug,omitempty"` + path string httpRunners map[string]*httpRunner dbRunners map[string]*dbRunner t *testing.T @@ -68,6 +69,7 @@ func LoadBook(path string) (*book, error) { _ = f.Close() return nil, err } + bk.path = path if err := f.Close(); err != nil { return nil, err } diff --git a/include.go b/include.go new file mode 100644 index 00000000..db793bf6 --- /dev/null +++ b/include.go @@ -0,0 +1,52 @@ +package runn + +import ( + "context" + "path/filepath" +) + +const includeRunnerKey = "include" + +type includeRunner struct { + operator *operator +} + +func newIncludeRunner(o *operator) (*includeRunner, error) { + return &includeRunner{ + operator: o, + }, nil +} + +func (rnr *includeRunner) Run(ctx context.Context, path string) error { + oo, err := rnr.operator.newNestedOperator(Book(filepath.Join(rnr.operator.root, path))) + if err != nil { + return err + } + if err := oo.Run(ctx); err != nil { + return err + } + rnr.operator.store.steps = append(rnr.operator.store.steps, map[string]interface{}{ + "steps": oo.store.steps, + }) + return nil +} + +func (o *operator) newNestedOperator(opts ...Option) (*operator, error) { + for k, r := range o.httpRunners { + opts = append(opts, HTTPRunner(k, r.endpoint.String(), r.client)) + } + for k, r := range o.dbRunners { + opts = append(opts, DBRunner(k, r.client)) + } + for k, v := range o.store.vars { + opts = append(opts, Var(k, v)) + } + opts = append(opts, Var("parent", o.store.steps)) + opts = append(opts, Debug(o.debug)) + oo, err := New(opts...) + if err != nil { + return nil, err + } + oo.t = o.t + return oo, nil +} diff --git a/include_test.go b/include_test.go new file mode 100644 index 00000000..10ba9b00 --- /dev/null +++ b/include_test.go @@ -0,0 +1,49 @@ +package runn + +import ( + "context" + "fmt" + "os" + "testing" +) + +func TestIncludeRunnerRun(t *testing.T) { + tests := []struct { + book string + want int + }{ + {"testdata/book/db.yml", 8}, + } + ctx := context.Background() + for _, tt := range tests { + db, err := os.CreateTemp("", "tmp") + if err != nil { + t.Fatal(err) + } + defer os.Remove(db.Name()) + o, err := New(Runner("db", fmt.Sprintf("sqlite://%s", db.Name()))) + if err != nil { + t.Fatal(err) + } + r, err := newIncludeRunner(o) + if err != nil { + t.Fatal(err) + } + if err := r.Run(ctx, tt.book); err != nil { + t.Fatal(err) + } + + { + got := len(r.operator.store.steps) + if want := 1; got != want { + t.Errorf("got %v\nwant %v", got, want) + } + } + { + got := len(r.operator.store.steps[0]["steps"].([]map[string]interface{})) + if got != tt.want { + t.Errorf("got %v\nwant %v", got, tt.want) + } + } + } +} diff --git a/operator.go b/operator.go index 7043fd98..7745f8f5 100644 --- a/operator.go +++ b/operator.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "path/filepath" "regexp" "strconv" "strings" @@ -17,15 +18,17 @@ import ( var expandRe = regexp.MustCompile(`"?{{\s*([^}]+)\s*}}"?`) type step struct { - httpRunner *httpRunner - httpRequest map[string]interface{} - dbRunner *dbRunner - dbQuery map[string]interface{} - testRunner *testRunner - testCond string - dumpRunner *dumpRunner - dumpCond string - debug bool + httpRunner *httpRunner + httpRequest map[string]interface{} + dbRunner *dbRunner + dbQuery map[string]interface{} + testRunner *testRunner + testCond string + dumpRunner *dumpRunner + dumpCond string + includeRunner *includeRunner + includePath string + debug bool } type store struct { @@ -40,6 +43,7 @@ type operator struct { store store desc string debug bool + root string t *testing.T } @@ -50,6 +54,7 @@ func New(opts ...Option) (*operator, error) { return nil, err } } + o := &operator{ httpRunners: map[string]*httpRunner{}, dbRunners: map[string]*dbRunner{}, @@ -62,12 +67,20 @@ func New(opts ...Option) (*operator, error) { t: bk.t, } + if bk.path != "" { + o.root = filepath.Dir(bk.path) + } else { + wd, err := os.Getwd() + if err != nil { + return nil, err + } + o.root = wd + } + for k, v := range bk.Runners { switch { - case k == testRunnerKey: - return nil, fmt.Errorf("runners[%s] is reserved as test runner", testRunnerKey) - case k == dumpRunnerKey: - return nil, fmt.Errorf("runners[%s] is reserved as dump runner", dumpRunnerKey) + case k == includeRunnerKey || k == testRunnerKey || k == dumpRunnerKey: + return nil, fmt.Errorf("runner name '%s' is reserved for built-in runner", k) case strings.Index(v, "https://") == 0 || strings.Index(v, "http://") == 0: hc, err := newHTTPRunner(k, v, o) if err != nil { @@ -135,6 +148,19 @@ func (o *operator) AppendStep(s map[string]interface{}) error { step.dumpCond = vv continue } + if k == includeRunnerKey { + ir, err := newIncludeRunner(o) + if err != nil { + return err + } + step.includeRunner = ir + vv, ok := v.(string) + if !ok { + return fmt.Errorf("invalid include path: %v", v) + } + step.includePath = vv + continue + } h, ok := o.httpRunners[k] if ok { step.httpRunner = h @@ -221,14 +247,21 @@ func (o *operator) run(ctx context.Context) error { _, _ = fmt.Fprintf(os.Stderr, "Run '%s' on steps[%d]\n", testRunnerKey, i) } if err := s.testRunner.Run(ctx, s.testCond); err != nil { - return fmt.Errorf("test failed on steps[%d]: %s", i, s.testCond) + return fmt.Errorf("test failed on steps[%d]: %v", i, err) } case s.dumpRunner != nil && s.dumpCond != "": if o.debug { _, _ = fmt.Fprintf(os.Stderr, "Run '%s' on steps[%d]\n", dumpRunnerKey, i) } if err := s.dumpRunner.Run(ctx, s.dumpCond); err != nil { - return fmt.Errorf("dump failed on steps[%d]: %s", i, s.dumpCond) + return fmt.Errorf("dump failed on steps[%d]: %v", i, err) + } + case s.includeRunner != nil && s.includePath != "": + if o.debug { + _, _ = fmt.Fprintf(os.Stderr, "Run '%s' on steps[%d]\n", includeRunnerKey, i) + } + if err := s.includeRunner.Run(ctx, s.includePath); err != nil { + return fmt.Errorf("include failed on steps[%d]: %v", i, err) } default: return fmt.Errorf("invalid steps[%d]: %v", i, s) diff --git a/operator_test.go b/operator_test.go index 39ffce29..05c909a8 100644 --- a/operator_test.go +++ b/operator_test.go @@ -159,8 +159,8 @@ func TestLoad(t *testing.T) { path string want int }{ - {"testdata/book/*", 3}, - {"testdata/**/*", 3}, + {"testdata/book/*", 4}, + {"testdata/**/*", 4}, } for _, tt := range tests { ops, err := Load(tt.path, Runner("req", "https://api.github.com"), Runner("db", "sqlite://path/to/test.db")) diff --git a/option.go b/option.go index 1ff365d2..c6436b5d 100644 --- a/option.go +++ b/option.go @@ -29,6 +29,7 @@ func Book(path string) Option { } bk.Steps = loaded.Steps bk.Debug = loaded.Debug + bk.path = loaded.path return nil } } diff --git a/testdata/book/db.yml b/testdata/book/db.yml index cdb683bf..b3873e0d 100644 --- a/testdata/book/db.yml +++ b/testdata/book/db.yml @@ -1,34 +1,22 @@ desc: Test using SQLite3 steps: - - db: - query: | - CREATE TABLE users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password TEXT NOT NULL, - email TEXT UNIQUE NOT NULL, - created NUMERIC NOT NULL, - updated NUMERIC - ) - - - db: - query: INSERT INTO users (username, password, email, created) VALUES ('alice', 'passw0rd', 'alice@example.com', datetime('2017-12-05')) + include: initdb.yml - db: query: SELECT * FROM users; - - test: 'steps[2].rows[0].username == "alice"' + test: 'steps[1].rows[0].username == "alice"' - db: - query: INSERT INTO users (username, password, email, created) VALUES ('bob', 'passw0rd', 'bob@example.com', datetime('2022-02-22')) + query: INSERT INTO users (username, password, email, created) VALUES ('charlie', 'passw0rd', 'charlie@example.com', datetime('2022-02-22')) - db: - query: SELECT * FROM users WHERE id = {{ steps[4].last_insert_id }} + query: SELECT * FROM users WHERE id = {{ steps[3].last_insert_id }} - - test: 'steps[5].rows[0].username == "bob"' + test: 'steps[4].rows[0].username == "charlie"' - db: query: SELECT COUNT(*) AS c FROM users - - test: 'steps[7].rows[0].c == 2' + test: 'steps[6].rows[0].c == 3' diff --git a/testdata/book/initdb.yml b/testdata/book/initdb.yml new file mode 100644 index 00000000..9237f1df --- /dev/null +++ b/testdata/book/initdb.yml @@ -0,0 +1,19 @@ +desc: Initialize SQLite3 +steps: + - + db: + query: | + CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + email TEXT UNIQUE NOT NULL, + created NUMERIC NOT NULL, + updated NUMERIC + ) + - + db: + query: INSERT INTO users (username, password, email, created) VALUES ('alice', 'passw0rd', 'alice@example.com', datetime('2017-12-05')) + - + db: + query: INSERT INTO users (username, password, email, created) VALUES ('bob', 'passw0rd', 'bob@example.com', datetime('2022-02-22'))