diff --git a/api/runnable.go b/api/runnable.go new file mode 100644 index 0000000..9e46f2f --- /dev/null +++ b/api/runnable.go @@ -0,0 +1,20 @@ +// Use and distribution licensed under the Apache license version 2. +// +// See the COPYING file in the root project directory for full text. + +package api + +import ( + "context" + "testing" +) + +// Runnable are things that Run a `*testing.T` +type Runnable interface { + // Run accepts a context and a `*testing.T` and runs some tests within that + // context + // + // Errors returned by Run() are **RuntimeErrors**, not failures in + // assertions. + Run(context.Context, *testing.T) error +} diff --git a/scenario/stub_fixtures_test.go b/internal/testutil/fixture/errstarter/fixture.go similarity index 86% rename from scenario/stub_fixtures_test.go rename to internal/testutil/fixture/errstarter/fixture.go index 4875b46..b38dd45 100644 --- a/scenario/stub_fixtures_test.go +++ b/internal/testutil/fixture/errstarter/fixture.go @@ -2,7 +2,7 @@ // // See the COPYING file in the root project directory for full text. -package scenario_test +package errstarter import ( "context" @@ -16,7 +16,7 @@ var ( return fmt.Errorf("error starting fixture!") } - errStarterFixture = fixture.New( + Fixture = fixture.New( fixture.WithStarter(errStarter), ) ) diff --git a/internal/testutil/plugin/bar/plugin.go b/internal/testutil/plugin/bar/plugin.go new file mode 100644 index 0000000..bfcd43c --- /dev/null +++ b/internal/testutil/plugin/bar/plugin.go @@ -0,0 +1,101 @@ +// Use and distribution licensed under the Apache license version 2. +// +// See the COPYING file in the root project directory for full text. + +package bar + +import ( + "context" + "strconv" + + "github.com/gdt-dev/gdt/api" + "github.com/gdt-dev/gdt/plugin" + "github.com/samber/lo" + "gopkg.in/yaml.v3" +) + +func init() { + plugin.Register(&Plugin{}) +} + +type Defaults struct { + Foo string `yaml:"bar"` +} + +func (d *Defaults) UnmarshalYAML(node *yaml.Node) error { + return nil +} + +type Spec struct { + api.Spec + Bar int `yaml:"bar"` +} + +func (s *Spec) SetBase(b api.Spec) { + s.Spec = b +} + +func (s *Spec) Base() *api.Spec { + return &s.Spec +} + +func (s *Spec) Retry() *api.Retry { + return api.NoRetry +} + +func (s *Spec) Timeout() *api.Timeout { + return nil +} + +func (s *Spec) Eval(context.Context) (*api.Result, error) { + return api.NewResult(), nil +} + +func (s *Spec) UnmarshalYAML(node *yaml.Node) error { + if node.Kind != yaml.MappingNode { + return api.ExpectedMapAt(node) + } + // maps/structs are stored in a top-level Node.Content field which is a + // concatenated slice of Node pointers in pairs of key/values. + for i := 0; i < len(node.Content); i += 2 { + keyNode := node.Content[i] + if keyNode.Kind != yaml.ScalarNode { + return api.ExpectedScalarAt(keyNode) + } + key := keyNode.Value + valNode := node.Content[i+1] + switch key { + case "bar": + if valNode.Kind != yaml.ScalarNode { + return api.ExpectedScalarAt(valNode) + } + if v, err := strconv.Atoi(valNode.Value); err != nil { + return api.ExpectedIntAt(valNode) + } else { + s.Bar = v + } + default: + if lo.Contains(api.BaseSpecFields, key) { + continue + } + return api.UnknownFieldAt(key, keyNode) + } + } + return nil +} + +type Plugin struct{} + +func (p *Plugin) Info() api.PluginInfo { + return api.PluginInfo{ + Name: "bar", + } +} + +func (p *Plugin) Defaults() yaml.Unmarshaler { + return &Defaults{} +} + +func (p *Plugin) Specs() []api.Evaluable { + return []api.Evaluable{&Spec{}} +} diff --git a/internal/testutil/plugin/failer/plugin.go b/internal/testutil/plugin/failer/plugin.go new file mode 100644 index 0000000..af42a9b --- /dev/null +++ b/internal/testutil/plugin/failer/plugin.go @@ -0,0 +1,136 @@ +// Use and distribution licensed under the Apache license version 2. +// +// See the COPYING file in the root project directory for full text. + +package failer + +import ( + "context" + "fmt" + "strconv" + + "github.com/gdt-dev/gdt/api" + gdtapi "github.com/gdt-dev/gdt/api" + "github.com/gdt-dev/gdt/plugin" + "github.com/samber/lo" + "gopkg.in/yaml.v3" +) + +func init() { + plugin.Register(&Plugin{}) +} + +type InnerDefaults struct { + Fail bool `yaml:"fail,omitempty"` +} + +type Defaults struct { + InnerDefaults +} + +func (d *Defaults) UnmarshalYAML(node *yaml.Node) error { + if node.Kind != yaml.MappingNode { + return api.ExpectedMapAt(node) + } + // maps/structs are stored in a top-level Node.Content field which is a + // concatenated slice of Node pointers in pairs of key/values. + for i := 0; i < len(node.Content); i += 2 { + keyNode := node.Content[i] + if keyNode.Kind != yaml.ScalarNode { + return api.ExpectedScalarAt(keyNode) + } + key := keyNode.Value + valNode := node.Content[i+1] + switch key { + case "fail": + if valNode.Kind != yaml.MappingNode { + return api.ExpectedMapAt(valNode) + } + inner := InnerDefaults{} + if err := valNode.Decode(&inner); err != nil { + return err + } + d.InnerDefaults = inner + // This is just for testing api when parsing defaults... + if d.Fail { + return fmt.Errorf("defaults parsing failed") + } + default: + continue + } + } + return nil +} + +type Spec struct { + api.Spec + Fail bool `yaml:"fail"` +} + +func (s *Spec) SetBase(b api.Spec) { + s.Spec = b +} + +func (s *Spec) Base() *api.Spec { + return &s.Spec +} + +func (s *Spec) Retry() *api.Retry { + return nil +} + +func (s *Spec) Timeout() *api.Timeout { + return nil +} + +func (s *Spec) Eval(context.Context) (*api.Result, error) { + return nil, fmt.Errorf("%w: Indy, bad dates!", gdtapi.RuntimeError) +} + +func (s *Spec) UnmarshalYAML(node *yaml.Node) error { + if node.Kind != yaml.MappingNode { + return api.ExpectedMapAt(node) + } + // maps/structs are stored in a top-level Node.Content field which is a + // concatenated slice of Node pointers in pairs of key/values. + for i := 0; i < len(node.Content); i += 2 { + keyNode := node.Content[i] + if keyNode.Kind != yaml.ScalarNode { + return api.ExpectedScalarAt(keyNode) + } + key := keyNode.Value + valNode := node.Content[i+1] + switch key { + case "fail": + if valNode.Kind != yaml.ScalarNode { + return api.ExpectedScalarAt(valNode) + } + s.Fail, _ = strconv.ParseBool(valNode.Value) + if s.Fail { + return fmt.Errorf("Indy, bad parse!") + } + default: + if lo.Contains(api.BaseSpecFields, key) { + continue + } + return api.UnknownFieldAt(key, keyNode) + } + } + return nil +} + +type Plugin struct{} + +func (p *Plugin) Info() api.PluginInfo { + return api.PluginInfo{ + Name: "fail", + } +} + +func (p *Plugin) Defaults() yaml.Unmarshaler { + return &Defaults{} +} + +func (p *Plugin) Specs() []api.Evaluable { + return []api.Evaluable{&Spec{}} +} diff --git a/internal/testutil/plugin/foo/plugin.go b/internal/testutil/plugin/foo/plugin.go new file mode 100644 index 0000000..b62c5d5 --- /dev/null +++ b/internal/testutil/plugin/foo/plugin.go @@ -0,0 +1,139 @@ +// Use and distribution licensed under the Apache license version 2. +// +// See the COPYING file in the root project directory for full text. + +package foo + +import ( + "context" + "fmt" + + "github.com/gdt-dev/gdt/api" + "github.com/gdt-dev/gdt/debug" + "github.com/gdt-dev/gdt/plugin" + "github.com/samber/lo" + "gopkg.in/yaml.v3" +) + +func init() { + plugin.Register(&Plugin{}) +} + +type InnerDefaults struct { + Bar string `yaml:"bar,omitempty"` +} + +type Defaults struct { + InnerDefaults `yaml:",inline"` +} + +func (d *Defaults) UnmarshalYAML(node *yaml.Node) error { + if node.Kind != yaml.MappingNode { + return api.ExpectedMapAt(node) + } + // maps/structs are stored in a top-level Node.Content field which is a + // concatenated slice of Node pointers in pairs of key/values. + for i := 0; i < len(node.Content); i += 2 { + keyNode := node.Content[i] + if keyNode.Kind != yaml.ScalarNode { + return api.ExpectedScalarAt(keyNode) + } + key := keyNode.Value + valNode := node.Content[i+1] + switch key { + case "foo": + if valNode.Kind != yaml.MappingNode { + return api.ExpectedMapAt(valNode) + } + inner := InnerDefaults{} + if err := valNode.Decode(&inner); err != nil { + return err + } + d.InnerDefaults = inner + default: + continue + } + } + return nil +} + +type Spec struct { + api.Spec + Foo string `yaml:"foo"` +} + +func (s *Spec) SetBase(b api.Spec) { + s.Spec = b +} + +func (s *Spec) Base() *api.Spec { + return &s.Spec +} + +func (s *Spec) Retry() *api.Retry { + return nil +} + +func (s *Spec) Timeout() *api.Timeout { + return nil +} + +func (s *Spec) UnmarshalYAML(node *yaml.Node) error { + if node.Kind != yaml.MappingNode { + return api.ExpectedMapAt(node) + } + // maps/structs are stored in a top-level Node.Content field which is a + // concatenated slice of Node pointers in pairs of key/values. + for i := 0; i < len(node.Content); i += 2 { + keyNode := node.Content[i] + if keyNode.Kind != yaml.ScalarNode { + return api.ExpectedScalarAt(keyNode) + } + key := keyNode.Value + valNode := node.Content[i+1] + switch key { + case "foo": + if valNode.Kind != yaml.ScalarNode { + return api.ExpectedScalarAt(valNode) + } + s.Foo = valNode.Value + default: + if lo.Contains(api.BaseSpecFields, key) { + continue + } + return api.UnknownFieldAt(key, keyNode) + } + } + return nil +} + +func (s *Spec) Eval(ctx context.Context) (*api.Result, error) { + fails := []error{} + debug.Println(ctx, "in %s Foo=%s", s.Title(), s.Foo) + // This is just a silly test to demonstrate how to write Eval() methods + // for plugin Spec specialization classes. + if s.Name == "bar" && s.Foo != "bar" { + fail := fmt.Errorf("expected s.Foo = 'bar', got %s", s.Foo) + fails = append(fails, fail) + } else if s.Name != "bar" && s.Foo != "baz" { + fail := fmt.Errorf("expected s.Foo = 'baz', got %s", s.Foo) + fails = append(fails, fail) + } + return api.NewResult(api.WithFailures(fails...)), nil +} + +type Plugin struct{} + +func (p *Plugin) Info() api.PluginInfo { + return api.PluginInfo{ + Name: "foo", + } +} + +func (p *Plugin) Defaults() yaml.Unmarshaler { + return &Defaults{} +} + +func (p *Plugin) Specs() []api.Evaluable { + return []api.Evaluable{&Spec{}} +} diff --git a/internal/testutil/plugin/priorrun/plugin.go b/internal/testutil/plugin/priorrun/plugin.go new file mode 100644 index 0000000..5d9aa47 --- /dev/null +++ b/internal/testutil/plugin/priorrun/plugin.go @@ -0,0 +1,124 @@ +// Use and distribution licensed under the Apache license version 2. +// +// See the COPYING file in the root project directory for full text. + +package priorrun + +import ( + "context" + "fmt" + + "github.com/gdt-dev/gdt/api" + gdtcontext "github.com/gdt-dev/gdt/context" + "github.com/gdt-dev/gdt/plugin" + "github.com/samber/lo" + "gopkg.in/yaml.v3" +) + +func init() { + plugin.Register(&Plugin{}) +} + +const PriorRunDataKey = "priorrun" + +type Defaults struct{} + +func (d *Defaults) UnmarshalYAML(node *yaml.Node) error { + return nil +} + +type Spec struct { + api.Spec + State string `yaml:"state"` + Prior string `yaml:"prior"` +} + +func (s *Spec) SetBase(b api.Spec) { + s.Spec = b +} + +func (s *Spec) Base() *api.Spec { + return &s.Spec +} + +func (s *Spec) Retry() *api.Retry { + return nil +} + +func (s *Spec) Timeout() *api.Timeout { + return nil +} + +func (s *Spec) UnmarshalYAML(node *yaml.Node) error { + if node.Kind != yaml.MappingNode { + return api.ExpectedMapAt(node) + } + // maps/structs are stored in a top-level Node.Content field which is a + // concatenated slice of Node pointers in pairs of key/values. + for i := 0; i < len(node.Content); i += 2 { + keyNode := node.Content[i] + if keyNode.Kind != yaml.ScalarNode { + return api.ExpectedScalarAt(keyNode) + } + key := keyNode.Value + valNode := node.Content[i+1] + switch key { + case "state": + if valNode.Kind != yaml.ScalarNode { + return api.ExpectedScalarAt(valNode) + } + s.State = valNode.Value + case "prior": + if valNode.Kind != yaml.ScalarNode { + return api.ExpectedScalarAt(valNode) + } + s.Prior = valNode.Value + default: + if lo.Contains(api.BaseSpecFields, key) { + continue + } + return api.UnknownFieldAt(key, keyNode) + } + } + return nil +} + +func (s *Spec) Eval(ctx context.Context) (*api.Result, error) { + // Here we test that the prior run data that we save at the end of each + // Run() is showing up properly in the next Run()'s context. + fails := []error{} + prData := gdtcontext.PriorRun(ctx) + if s.Index == 0 { + if len(prData) != 0 { + fails = append(fails, fmt.Errorf("expected prData to be empty")) + } + } else { + data, ok := prData[PriorRunDataKey] + if !ok { + fails = append(fails, fmt.Errorf("expected PriorRunDataKey in priorRun map")) + } + if s.Prior != data { + fails = append(fails, fmt.Errorf("expected priorRunData == data")) + } + } + return api.NewResult( + api.WithFailures(fails...), + api.WithData(PriorRunDataKey, s.State), + ), nil +} + +type Plugin struct{} + +func (p *Plugin) Info() api.PluginInfo { + return api.PluginInfo{ + Name: "priorRun", + } +} + +func (p *Plugin) Defaults() yaml.Unmarshaler { + return &Defaults{} +} + +func (p *Plugin) Specs() []api.Evaluable { + return []api.Evaluable{&Spec{}} +} diff --git a/porcelain.go b/porcelain.go index 0d7d7b4..d2b6c18 100644 --- a/porcelain.go +++ b/porcelain.go @@ -89,10 +89,10 @@ var ( NewJSONFixture = jsonfix.New ) -// From returns a new suite.Suite from an `io.Reader`, a string file or +// From returns a new `api.Runnable` from an `io.Reader`, a string file or // directory path, or the raw bytes of YAML content describing a scenario or // suite. -func From(source interface{}) (*suite.Suite, error) { +func From(source interface{}) (api.Runnable, error) { switch src := source.(type) { case io.Reader: s, err := scenario.FromReader(src) diff --git a/porcelain_test.go b/porcelain_test.go index 3edac1a..ad451f0 100644 --- a/porcelain_test.go +++ b/porcelain_test.go @@ -12,6 +12,7 @@ import ( "github.com/gdt-dev/gdt" "github.com/gdt-dev/gdt/api" + "github.com/gdt-dev/gdt/suite" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -39,27 +40,35 @@ func TestFromFileNotFound(t *testing.T) { assert.True(os.IsNotExist(err)) } -func TestFromSuite(t *testing.T) { +func TestFromSuitePath(t *testing.T) { assert := assert.New(t) require := require.New(t) fp := filepath.Join("suite", "testdata", "exec") - suite, err := gdt.From(fp) + r, err := gdt.From(fp) require.Nil(err) - require.NotNil(suite) + require.NotNil(r) - assert.Equal(fp, suite.Path) - assert.Len(suite.Scenarios, 2) + assert.IsType(r, &suite.Suite{}) + + s := r.(*suite.Suite) + + assert.Equal(fp, s.Path) + assert.Len(s.Scenarios, 2) } -func TestFromScenarioPath(t *testing.T) { +func TestFromFilePath(t *testing.T) { assert := assert.New(t) require := require.New(t) fp := filepath.Join("suite", "testdata", "exec", "ls.yaml") - s, err := gdt.From(fp) + r, err := gdt.From(fp) require.Nil(err) - require.NotNil(s) + require.NotNil(r) + + assert.IsType(r, &suite.Suite{}) + + s := r.(*suite.Suite) assert.Equal(fp, s.Path) assert.Len(s.Scenarios, 1) @@ -74,18 +83,24 @@ func TestFromScenarioReader(t *testing.T) { fp := filepath.Join("suite", "testdata", "exec", "ls.yaml") f, err := os.Open(fp) require.Nil(err) - suite, err := gdt.From(f) - assert.Nil(err) - assert.NotNil(suite) + + r, err := gdt.From(f) + require.Nil(err) + require.NotNil(r) + + assert.IsType(r, &suite.Suite{}) + + s := r.(*suite.Suite) // The scenario's path isn't set because we didn't supply a filepath... - assert.Equal("", suite.Path) - assert.Len(suite.Scenarios, 1) - assert.Len(suite.Scenarios[0].Tests, 1) + assert.Equal("", s.Path) + assert.Len(s.Scenarios, 1) + assert.Len(s.Scenarios[0].Tests, 1) } func TestFromScenarioBytes(t *testing.T) { assert := assert.New(t) + require := require.New(t) raw := `name: foo description: simple foo test @@ -93,14 +108,19 @@ tests: - exec: echo foo ` b := []byte(raw) - suite, err := gdt.From(b) - assert.Nil(err) - assert.NotNil(suite) + + r, err := gdt.From(b) + require.Nil(err) + require.NotNil(r) + + assert.IsType(r, &suite.Suite{}) + + s := r.(*suite.Suite) // The scenario's path isn't set because we didn't supply a filepath... - assert.Equal("", suite.Path) - assert.Len(suite.Scenarios, 1) - assert.Len(suite.Scenarios[0].Tests, 1) + assert.Equal("", s.Path) + assert.Len(s.Scenarios, 1) + assert.Len(s.Scenarios[0].Tests, 1) } func TestRunExecSuite(t *testing.T) { diff --git a/scenario/parse_test.go b/scenario/parse_test.go index 76de4e0..2cf041e 100644 --- a/scenario/parse_test.go +++ b/scenario/parse_test.go @@ -13,6 +13,11 @@ import ( "github.com/gdt-dev/gdt/scenario" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/gdt-dev/gdt/internal/testutil/plugin/bar" + "github.com/gdt-dev/gdt/internal/testutil/plugin/failer" + "github.com/gdt-dev/gdt/internal/testutil/plugin/foo" + "github.com/gdt-dev/gdt/internal/testutil/plugin/priorrun" ) func TestFailingDefaults(t *testing.T) { @@ -49,14 +54,16 @@ func TestNoTests(t *testing.T) { assert.Equal([]string{"books_api", "books_data"}, s.Fixtures) assert.Equal( map[string]interface{}{ - "foo": &fooDefaults{ - fooInnerDefaults{ + "foo": &foo.Defaults{ + InnerDefaults: foo.InnerDefaults{ Bar: "barconfig", }, }, - "bar": &barDefaults{}, - "fail": &failDefaults{failInnerDefaults{}}, - "priorRun": &priorRunDefaults{}, + "bar": &bar.Defaults{}, + "fail": &failer.Defaults{ + InnerDefaults: failer.InnerDefaults{}, + }, + "priorRun": &priorrun.Defaults{}, scenario.DefaultsKey: &scenario.Defaults{}, }, s.Defaults, @@ -199,31 +206,35 @@ func TestKnownSpec(t *testing.T) { assert.Empty(s.Fixtures) assert.Equal( map[string]interface{}{ - "foo": &fooDefaults{ - fooInnerDefaults{ + "foo": &foo.Defaults{ + InnerDefaults: foo.InnerDefaults{ Bar: "barconfig", }, }, - "bar": &barDefaults{}, - "fail": &failDefaults{failInnerDefaults{}}, - "priorRun": &priorRunDefaults{}, + "bar": &bar.Defaults{}, + "fail": &failer.Defaults{ + InnerDefaults: failer.InnerDefaults{}, + }, + "priorRun": &priorrun.Defaults{}, scenario.DefaultsKey: &scenario.Defaults{}, }, s.Defaults, ) expSpecDefaults := &api.Defaults{ - "foo": &fooDefaults{ - fooInnerDefaults{ + "foo": &foo.Defaults{ + InnerDefaults: foo.InnerDefaults{ Bar: "barconfig", }, }, - "bar": &barDefaults{}, - "fail": &failDefaults{failInnerDefaults{}}, - "priorRun": &priorRunDefaults{}, + "bar": &bar.Defaults{}, + "fail": &failer.Defaults{ + InnerDefaults: failer.InnerDefaults{}, + }, + "priorRun": &priorrun.Defaults{}, scenario.DefaultsKey: &scenario.Defaults{}, } expTests := []api.Evaluable{ - &fooSpec{ + &foo.Spec{ Spec: api.Spec{ Index: 0, Name: "bar", @@ -231,7 +242,7 @@ func TestKnownSpec(t *testing.T) { }, Foo: "bar", }, - &fooSpec{ + &foo.Spec{ Spec: api.Spec{ Index: 1, Description: "Bazzy Bizzy", @@ -259,14 +270,14 @@ func TestMultipleSpec(t *testing.T) { assert.Equal("foo-bar", s.Name) assert.Equal(filepath.Join("testdata", "foo-bar.yaml"), s.Path) expTests := []api.Evaluable{ - &fooSpec{ + &foo.Spec{ Spec: api.Spec{ Index: 0, Defaults: &api.Defaults{}, }, Foo: "bar", }, - &barSpec{ + &bar.Spec{ Spec: api.Spec{ Index: 1, Defaults: &api.Defaults{}, @@ -299,31 +310,35 @@ func TestEnvExpansion(t *testing.T) { assert.Empty(s.Fixtures) assert.Equal( map[string]interface{}{ - "foo": &fooDefaults{ - fooInnerDefaults{ + "foo": &foo.Defaults{ + InnerDefaults: foo.InnerDefaults{ Bar: "barconfig", }, }, - "bar": &barDefaults{}, - "fail": &failDefaults{failInnerDefaults{}}, - "priorRun": &priorRunDefaults{}, + "bar": &bar.Defaults{}, + "fail": &failer.Defaults{ + InnerDefaults: failer.InnerDefaults{}, + }, + "priorRun": &priorrun.Defaults{}, scenario.DefaultsKey: &scenario.Defaults{}, }, s.Defaults, ) expSpecDefaults := &api.Defaults{ - "foo": &fooDefaults{ - fooInnerDefaults{ + "foo": &foo.Defaults{ + InnerDefaults: foo.InnerDefaults{ Bar: "barconfig", }, }, - "bar": &barDefaults{}, - "fail": &failDefaults{failInnerDefaults{}}, - "priorRun": &priorRunDefaults{}, + "bar": &bar.Defaults{}, + "fail": &failer.Defaults{ + InnerDefaults: failer.InnerDefaults{}, + }, + "priorRun": &priorrun.Defaults{}, scenario.DefaultsKey: &scenario.Defaults{}, } expTests := []api.Evaluable{ - &fooSpec{ + &foo.Spec{ Spec: api.Spec{ Index: 0, Name: "$NOT_EXPANDED", @@ -331,7 +346,7 @@ func TestEnvExpansion(t *testing.T) { }, Foo: "bar", }, - &fooSpec{ + &foo.Spec{ Spec: api.Spec{ Index: 1, Description: "Bazzy Bizzy", @@ -361,10 +376,12 @@ func TestScenarioDefaults(t *testing.T) { assert.Empty(s.Fixtures) assert.Equal( map[string]interface{}{ - "foo": &fooDefaults{}, - "bar": &barDefaults{}, - "fail": &failDefaults{failInnerDefaults{}}, - "priorRun": &priorRunDefaults{}, + "foo": &foo.Defaults{}, + "bar": &bar.Defaults{}, + "fail": &failer.Defaults{ + InnerDefaults: failer.InnerDefaults{}, + }, + "priorRun": &priorrun.Defaults{}, scenario.DefaultsKey: &scenario.Defaults{ Timeout: &api.Timeout{ After: "2s", @@ -374,10 +391,12 @@ func TestScenarioDefaults(t *testing.T) { s.Defaults, ) expSpecDefaults := &api.Defaults{ - "foo": &fooDefaults{}, - "bar": &barDefaults{}, - "fail": &failDefaults{failInnerDefaults{}}, - "priorRun": &priorRunDefaults{}, + "foo": &foo.Defaults{}, + "bar": &bar.Defaults{}, + "fail": &failer.Defaults{ + InnerDefaults: failer.InnerDefaults{}, + }, + "priorRun": &priorrun.Defaults{}, scenario.DefaultsKey: &scenario.Defaults{ Timeout: &api.Timeout{ After: "2s", @@ -385,7 +404,7 @@ func TestScenarioDefaults(t *testing.T) { }, } expTests := []api.Evaluable{ - &fooSpec{ + &foo.Spec{ Spec: api.Spec{ Index: 0, Defaults: expSpecDefaults, @@ -395,7 +414,7 @@ func TestScenarioDefaults(t *testing.T) { }, Foo: "baz", }, - &fooSpec{ + &foo.Spec{ Spec: api.Spec{ Index: 1, Defaults: expSpecDefaults, diff --git a/scenario/run_test.go b/scenario/run_test.go index 8b951a9..ec75a7c 100644 --- a/scenario/run_test.go +++ b/scenario/run_test.go @@ -19,6 +19,8 @@ import ( "github.com/gdt-dev/gdt/scenario" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/gdt-dev/gdt/internal/testutil/fixture/errstarter" ) var failFlag = flag.Bool("fail", false, "run tests expected to fail") @@ -85,7 +87,7 @@ func TestFixtureStartError(t *testing.T) { require.NotNil(s) ctx := gdtcontext.New() - ctx = gdtcontext.RegisterFixture(ctx, "start-error", errStarterFixture) + ctx = gdtcontext.RegisterFixture(ctx, "start-error", errstarter.Fixture) err = s.Run(ctx, t) assert.NotNil(err) diff --git a/scenario/stub_plugins_test.go b/scenario/stub_plugins_test.go deleted file mode 100644 index d11954e..0000000 --- a/scenario/stub_plugins_test.go +++ /dev/null @@ -1,446 +0,0 @@ -// Use and distribution licensed under the Apache license version 2. -// -// See the COPYING file in the root project directory for full text. - -package scenario_test - -import ( - "context" - "fmt" - "strconv" - - "github.com/gdt-dev/gdt/api" - gdtapi "github.com/gdt-dev/gdt/api" - gdtcontext "github.com/gdt-dev/gdt/context" - "github.com/gdt-dev/gdt/debug" - "github.com/gdt-dev/gdt/plugin" - "github.com/samber/lo" - "gopkg.in/yaml.v3" -) - -func init() { - plugin.Register(&fooPlugin{}) - plugin.Register(&barPlugin{}) - plugin.Register(&failingPlugin{}) - plugin.Register(&priorRunPlugin{}) -} - -type failInnerDefaults struct { - Fail bool `yaml:"fail,omitempty"` -} - -type failDefaults struct { - failInnerDefaults -} - -func (d *failDefaults) UnmarshalYAML(node *yaml.Node) error { - if node.Kind != yaml.MappingNode { - return api.ExpectedMapAt(node) - } - // maps/structs are stored in a top-level Node.Content field which is a - // concatenated slice of Node pointers in pairs of key/values. - for i := 0; i < len(node.Content); i += 2 { - keyNode := node.Content[i] - if keyNode.Kind != yaml.ScalarNode { - return api.ExpectedScalarAt(keyNode) - } - key := keyNode.Value - valNode := node.Content[i+1] - switch key { - case "fail": - if valNode.Kind != yaml.MappingNode { - return api.ExpectedMapAt(valNode) - } - inner := failInnerDefaults{} - if err := valNode.Decode(&inner); err != nil { - return err - } - d.failInnerDefaults = inner - // This is just for testing api when parsing defaults... - if d.Fail { - return fmt.Errorf("defaults parsing failed") - } - default: - continue - } - } - return nil -} - -type failSpec struct { - api.Spec - Fail bool `yaml:"fail"` -} - -func (s *failSpec) SetBase(b api.Spec) { - s.Spec = b -} - -func (s *failSpec) Base() *api.Spec { - return &s.Spec -} - -func (s *failSpec) Retry() *api.Retry { - return nil -} - -func (s *failSpec) Timeout() *api.Timeout { - return nil -} - -func (s *failSpec) Eval(context.Context) (*api.Result, error) { - return nil, fmt.Errorf("%w: Indy, bad dates!", gdtapi.RuntimeError) -} - -func (s *failSpec) UnmarshalYAML(node *yaml.Node) error { - if node.Kind != yaml.MappingNode { - return api.ExpectedMapAt(node) - } - // maps/structs are stored in a top-level Node.Content field which is a - // concatenated slice of Node pointers in pairs of key/values. - for i := 0; i < len(node.Content); i += 2 { - keyNode := node.Content[i] - if keyNode.Kind != yaml.ScalarNode { - return api.ExpectedScalarAt(keyNode) - } - key := keyNode.Value - valNode := node.Content[i+1] - switch key { - case "fail": - if valNode.Kind != yaml.ScalarNode { - return api.ExpectedScalarAt(valNode) - } - s.Fail, _ = strconv.ParseBool(valNode.Value) - if s.Fail { - return fmt.Errorf("Indy, bad parse!") - } - default: - if lo.Contains(api.BaseSpecFields, key) { - continue - } - return api.UnknownFieldAt(key, keyNode) - } - } - return nil -} - -type failingPlugin struct{} - -func (p *failingPlugin) Info() api.PluginInfo { - return api.PluginInfo{ - Name: "fail", - } -} - -func (p *failingPlugin) Defaults() yaml.Unmarshaler { - return &failDefaults{} -} - -func (p *failingPlugin) Specs() []api.Evaluable { - return []api.Evaluable{&failSpec{}} -} - -type fooInnerDefaults struct { - Bar string `yaml:"bar,omitempty"` -} - -type fooDefaults struct { - fooInnerDefaults -} - -func (d *fooDefaults) UnmarshalYAML(node *yaml.Node) error { - if node.Kind != yaml.MappingNode { - return api.ExpectedMapAt(node) - } - // maps/structs are stored in a top-level Node.Content field which is a - // concatenated slice of Node pointers in pairs of key/values. - for i := 0; i < len(node.Content); i += 2 { - keyNode := node.Content[i] - if keyNode.Kind != yaml.ScalarNode { - return api.ExpectedScalarAt(keyNode) - } - key := keyNode.Value - valNode := node.Content[i+1] - switch key { - case "foo": - if valNode.Kind != yaml.MappingNode { - return api.ExpectedMapAt(valNode) - } - inner := fooInnerDefaults{} - if err := valNode.Decode(&inner); err != nil { - return err - } - d.fooInnerDefaults = inner - default: - continue - } - } - return nil -} - -type fooSpec struct { - api.Spec - Foo string `yaml:"foo"` -} - -func (s *fooSpec) SetBase(b api.Spec) { - s.Spec = b -} - -func (s *fooSpec) Base() *api.Spec { - return &s.Spec -} - -func (s *fooSpec) Retry() *api.Retry { - return nil -} - -func (s *fooSpec) Timeout() *api.Timeout { - return nil -} - -func (s *fooSpec) UnmarshalYAML(node *yaml.Node) error { - if node.Kind != yaml.MappingNode { - return api.ExpectedMapAt(node) - } - // maps/structs are stored in a top-level Node.Content field which is a - // concatenated slice of Node pointers in pairs of key/values. - for i := 0; i < len(node.Content); i += 2 { - keyNode := node.Content[i] - if keyNode.Kind != yaml.ScalarNode { - return api.ExpectedScalarAt(keyNode) - } - key := keyNode.Value - valNode := node.Content[i+1] - switch key { - case "foo": - if valNode.Kind != yaml.ScalarNode { - return api.ExpectedScalarAt(valNode) - } - s.Foo = valNode.Value - default: - if lo.Contains(api.BaseSpecFields, key) { - continue - } - return api.UnknownFieldAt(key, keyNode) - } - } - return nil -} - -func (s *fooSpec) Eval(ctx context.Context) (*api.Result, error) { - fails := []error{} - debug.Println(ctx, "in %s Foo=%s", s.Title(), s.Foo) - // This is just a silly test to demonstrate how to write Eval() methods - // for plugin Spec specialization classes. - if s.Name == "bar" && s.Foo != "bar" { - fail := fmt.Errorf("expected s.Foo = 'bar', got %s", s.Foo) - fails = append(fails, fail) - } else if s.Name != "bar" && s.Foo != "baz" { - fail := fmt.Errorf("expected s.Foo = 'baz', got %s", s.Foo) - fails = append(fails, fail) - } - return api.NewResult(api.WithFailures(fails...)), nil -} - -type fooPlugin struct{} - -func (p *fooPlugin) Info() api.PluginInfo { - return api.PluginInfo{ - Name: "foo", - } -} - -func (p *fooPlugin) Defaults() yaml.Unmarshaler { - return &fooDefaults{} -} - -func (p *fooPlugin) Specs() []api.Evaluable { - return []api.Evaluable{&fooSpec{}} -} - -type barDefaults struct { - Foo string `yaml:"bar"` -} - -func (d *barDefaults) UnmarshalYAML(node *yaml.Node) error { - return nil -} - -type barSpec struct { - api.Spec - Bar int `yaml:"bar"` -} - -func (s *barSpec) SetBase(b api.Spec) { - s.Spec = b -} - -func (s *barSpec) Base() *api.Spec { - return &s.Spec -} - -func (s *barSpec) Retry() *api.Retry { - return api.NoRetry -} - -func (s *barSpec) Timeout() *api.Timeout { - return nil -} - -func (s *barSpec) Eval(context.Context) (*api.Result, error) { - return api.NewResult(), nil -} - -func (s *barSpec) UnmarshalYAML(node *yaml.Node) error { - if node.Kind != yaml.MappingNode { - return api.ExpectedMapAt(node) - } - // maps/structs are stored in a top-level Node.Content field which is a - // concatenated slice of Node pointers in pairs of key/values. - for i := 0; i < len(node.Content); i += 2 { - keyNode := node.Content[i] - if keyNode.Kind != yaml.ScalarNode { - return api.ExpectedScalarAt(keyNode) - } - key := keyNode.Value - valNode := node.Content[i+1] - switch key { - case "bar": - if valNode.Kind != yaml.ScalarNode { - return api.ExpectedScalarAt(valNode) - } - if v, err := strconv.Atoi(valNode.Value); err != nil { - return api.ExpectedIntAt(valNode) - } else { - s.Bar = v - } - default: - if lo.Contains(api.BaseSpecFields, key) { - continue - } - return api.UnknownFieldAt(key, keyNode) - } - } - return nil -} - -type barPlugin struct{} - -func (p *barPlugin) Info() api.PluginInfo { - return api.PluginInfo{ - Name: "bar", - } -} - -func (p *barPlugin) Defaults() yaml.Unmarshaler { - return &barDefaults{} -} - -func (p *barPlugin) Specs() []api.Evaluable { - return []api.Evaluable{&barSpec{}} -} - -const priorRunDataKey = "priorrun" - -type priorRunDefaults struct{} - -func (d *priorRunDefaults) UnmarshalYAML(node *yaml.Node) error { - return nil -} - -type priorRunSpec struct { - api.Spec - State string `yaml:"state"` - Prior string `yaml:"prior"` -} - -func (s *priorRunSpec) SetBase(b api.Spec) { - s.Spec = b -} - -func (s *priorRunSpec) Base() *api.Spec { - return &s.Spec -} - -func (s *priorRunSpec) Retry() *api.Retry { - return nil -} - -func (s *priorRunSpec) Timeout() *api.Timeout { - return nil -} - -func (s *priorRunSpec) UnmarshalYAML(node *yaml.Node) error { - if node.Kind != yaml.MappingNode { - return api.ExpectedMapAt(node) - } - // maps/structs are stored in a top-level Node.Content field which is a - // concatenated slice of Node pointers in pairs of key/values. - for i := 0; i < len(node.Content); i += 2 { - keyNode := node.Content[i] - if keyNode.Kind != yaml.ScalarNode { - return api.ExpectedScalarAt(keyNode) - } - key := keyNode.Value - valNode := node.Content[i+1] - switch key { - case "state": - if valNode.Kind != yaml.ScalarNode { - return api.ExpectedScalarAt(valNode) - } - s.State = valNode.Value - case "prior": - if valNode.Kind != yaml.ScalarNode { - return api.ExpectedScalarAt(valNode) - } - s.Prior = valNode.Value - default: - if lo.Contains(api.BaseSpecFields, key) { - continue - } - return api.UnknownFieldAt(key, keyNode) - } - } - return nil -} - -func (s *priorRunSpec) Eval(ctx context.Context) (*api.Result, error) { - // Here we test that the prior run data that we save at the end of each - // Run() is showing up properly in the next Run()'s context. - fails := []error{} - prData := gdtcontext.PriorRun(ctx) - if s.Index == 0 { - if len(prData) != 0 { - fails = append(fails, fmt.Errorf("expected prData to be empty")) - } - } else { - data, ok := prData[priorRunDataKey] - if !ok { - fails = append(fails, fmt.Errorf("expected priorRunDataKey in priorRun map")) - } - if s.Prior != data { - fails = append(fails, fmt.Errorf("expected priorRunData == data")) - } - } - return api.NewResult( - api.WithFailures(fails...), - api.WithData(priorRunDataKey, s.State), - ), nil -} - -type priorRunPlugin struct{} - -func (p *priorRunPlugin) Info() api.PluginInfo { - return api.PluginInfo{ - Name: "priorRun", - } -} - -func (p *priorRunPlugin) Defaults() yaml.Unmarshaler { - return &priorRunDefaults{} -} - -func (p *priorRunPlugin) Specs() []api.Evaluable { - return []api.Evaluable{&priorRunSpec{}} -} diff --git a/suite/from.go b/suite/from.go index d094452..e92a78b 100644 --- a/suite/from.go +++ b/suite/from.go @@ -70,10 +70,9 @@ func FromScenario(s *scenario.Scenario) *Suite { Path: s.Path, Name: suiteNameFromScenarioPath(s.Path), Description: s.Description, - // NOTE: require needs to be named to fixture? - Require: s.Fixtures, - Defaults: s.Defaults, - Scenarios: []*scenario.Scenario{s}, + Fixtures: s.Fixtures, + Defaults: s.Defaults, + Scenarios: []*scenario.Scenario{s}, } } diff --git a/suite/suite.go b/suite/suite.go index 38371c7..fcc3060 100644 --- a/suite/suite.go +++ b/suite/suite.go @@ -23,9 +23,9 @@ type Suite struct { // During parsing, plugins are handed this raw data and asked to interpret // it into known configuration values for that plugin. Defaults map[string]interface{} `yaml:"defaults,omitempty"` - // Require specifies an ordered list of fixtures the test suite's test - // cases depends on. - Require []string `yaml:"require,omitempty"` + // Fixtures specifies an ordered list of fixtures the test suite's test + // cases depend on. + Fixtures []string `yaml:"fixtures,omitempty"` // Scenarios is a collection of test scenarios in this test suite Scenarios []*scenario.Scenario `yaml:"-"` } @@ -61,10 +61,10 @@ func WithDefaults(defaults map[string]interface{}) SuiteModifier { } } -// WithRequires sets a test suite's Requires attribute -func WithRequires(require []string) SuiteModifier { +// WithFixtures sets a test suite's Fixtures attribute +func WithFixtures(fixtures []string) SuiteModifier { return func(s *Suite) { - s.Require = require + s.Fixtures = fixtures } }