Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: skip-if conditional scenario skip #5

Merged
merged 1 commit into from
Jul 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,9 @@ All `gdt` scenarios have the following fields:
and configuration values for that plugin.
* `fixtures`: (optional) list of strings indicating named fixtures that will be
started before any of the tests in the file are run
* `skip-if`: (optional) list of [`Spec`][basespec] specializations that will be
evaluated *before* running any test in the scenario. If any of these
conditions evaluates successfully, the test scenario will be skipped.
* `tests`: list of [`Spec`][basespec] specializations that represent the
runnable test units in the test scenario.

Expand Down
37 changes: 35 additions & 2 deletions scenario/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,9 @@ func (s *Scenario) UnmarshalYAML(node *yaml.Node) error {
return gdterrors.ExpectedScalarAt(keyNode)
}
key := keyNode.Value
if key == "tests" {
valNode := node.Content[i+1]
valNode := node.Content[i+1]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still need to dig deeper in the yaml library to understand why we do i += 2 and not just a i++.
But overall looks good!
/lgtm

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@a-hilaly yeah, it's weird... see the comment above:

	// maps/structs are stored in a top-level Node.Content field which is a
	// concatenated slice of Node pointers in pairs of key/values.

switch key {
case "tests":
if valNode.Kind != yaml.SequenceNode {
return gdterrors.ExpectedSequenceAt(valNode)
}
Expand Down Expand Up @@ -122,6 +123,38 @@ func (s *Scenario) UnmarshalYAML(node *yaml.Node) error {
return gdterrors.UnknownSpecAt(s.Path, valNode)
}
}
case "skip-if":
if valNode.Kind != yaml.SequenceNode {
return gdterrors.ExpectedSequenceAt(valNode)
}
for idx, testNode := range valNode.Content {
parsed := false
base := gdttypes.Spec{}
if err := testNode.Decode(&base); err != nil {
return err
}
base.Index = idx
base.Defaults = &defaults
specs := []gdttypes.Evaluable{}
for _, p := range plugins {
specs = append(specs, p.Specs()...)
}
for _, sp := range specs {
if err := testNode.Decode(sp); err != nil {
if errors.Is(err, gdterrors.ErrUnknownField) {
continue
}
return err
}
sp.SetBase(base)
s.SkipIf = append(s.SkipIf, sp)
parsed = true
break
}
if !parsed {
return gdterrors.UnknownSpecAt(s.Path, valNode)
}
}
}
}
return nil
Expand Down
16 changes: 16 additions & 0 deletions scenario/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,22 @@ func (s *Scenario) Run(ctx context.Context, t *testing.T) error {
if found {
scDefaults = scDefaultsAny.(*Defaults)
}
// If the test author has specified any pre-flight checks in the `skip-if`
// collection, evaluate those first and if any failed, skip the scenario's
// tests.
for _, skipIf := range s.SkipIf {
res := skipIf.Eval(ctx, t)
if res.HasRuntimeError() {
return res.RuntimeError()
}
if len(res.Failures()) == 0 {
t.Skipf(
"skip-if: %s passed. skipping test.",
skipIf.Base().Title(),
)
return nil
}
}
t.Run(s.Title(), func(t *testing.T) {
for _, spec := range s.Tests {
// Create a brand new context that inherits the top-level context's
Expand Down
39 changes: 18 additions & 21 deletions scenario/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,17 @@ package scenario_test

import (
"context"
"fmt"
"os"
"path/filepath"
"testing"

gdtcontext "github.com/gdt-dev/gdt/context"
"github.com/gdt-dev/gdt/debug"
gdterrors "github.com/gdt-dev/gdt/errors"
"github.com/gdt-dev/gdt/result"
"github.com/gdt-dev/gdt/scenario"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func (s *fooSpec) Eval(ctx context.Context, t *testing.T) *result.Result {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note to @a-hilaly I just moved this into the scenario/stub_plugins_test.go file along with all the other stub plugin and spec definitions used in unit testing.

fails := []error{}
t.Run(s.Title(), func(t *testing.T) {
debug.Printf(ctx, t, "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 result.New(result.WithFailures(fails...))
}

func TestRun(t *testing.T) {
require := require.New(t)

Expand All @@ -62,7 +42,8 @@ func TestPriorRun(t *testing.T) {
require.Nil(err)
require.NotNil(s)

s.Run(context.TODO(), t)
err = s.Run(context.TODO(), t)
require.Nil(err)
}

func TestMissingFixtures(t *testing.T) {
Expand Down Expand Up @@ -117,3 +98,19 @@ func TestTimeoutCascade(t *testing.T) {
err = s.Run(context.TODO(), t)
require.Nil(err)
}

func TestSkipIf(t *testing.T) {
require := require.New(t)

fp := filepath.Join("testdata", "skip-if.yaml")
f, err := os.Open(fp)
require.Nil(err)

s, err := scenario.FromReader(f, scenario.WithPath(fp))
require.Nil(err)
require.NotNil(s)

err = s.Run(context.TODO(), t)
require.Nil(err)
require.True(t.Skipped())
}
42 changes: 42 additions & 0 deletions scenario/scenario.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,48 @@ type Scenario struct {
Defaults map[string]interface{} `yaml:"defaults,omitempty"`
// Fixtures specifies an ordered list of fixtures the test case depends on.
Fixtures []string `yaml:"fixtures,omitempty"`
// SkipIf contains a list of evaluable conditions. If any of the conditions
// evaluates successfully, the test scenario will be skipped. This allows
// test authors to specify "pre-flight checks" that should pass before
// attempting any of the actions in the scenario's tests.
//
// For example, let's assume you have a `gdt-kube` scenario that looks like
// this:
//
// ```yaml
// tests:
// - kube.create: manifests/nginx-deployment.yaml
// - kube:
// get: deployments/nginx
// assert:
// matches:
// status:
// readyReplicas: 2
// - kube.delete: deployments/nginx
// ```
//
// If you execute the above test and there is already an 'nginx'
// deployment, the `kube.create` test will fail. To prevent the scenario
// from proceeding with the tests if an 'nginx' deployment already exists,
// you could add the following
//
// ```yaml
// skip-if:
// - kube.get: deployments/nginx
// tests:
// - kube.create: manifests/nginx-deployment.yaml
// - kube:
// get: deployments/nginx
// assert:
// matches:
// status:
// readyReplicas: 2
// - kube.delete: deployments/nginx
// ```
//
// With the above, if an 'nginx' deployment exists already, the scenario
// will skip all the tests.
SkipIf []gdttypes.Evaluable `yaml:"skip-if,omitempty"`
// Tests is the collection of test units in this test case. These will be
// the fully parsed and materialized plugin Spec structs.
Tests []gdttypes.Evaluable `yaml:"tests,omitempty"`
Expand Down
18 changes: 18 additions & 0 deletions scenario/stub_plugins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"testing"

gdtcontext "github.com/gdt-dev/gdt/context"
"github.com/gdt-dev/gdt/debug"
"github.com/gdt-dev/gdt/errors"
gdterrors "github.com/gdt-dev/gdt/errors"
"github.com/gdt-dev/gdt/plugin"
Expand Down Expand Up @@ -219,6 +220,23 @@ func (s *fooSpec) UnmarshalYAML(node *yaml.Node) error {
return nil
}

func (s *fooSpec) Eval(ctx context.Context, t *testing.T) *result.Result {
fails := []error{}
t.Run(s.Title(), func(t *testing.T) {
debug.Printf(ctx, t, "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 result.New(result.WithFailures(fails...))
}

type fooPlugin struct{}

func (p *fooPlugin) Info() gdttypes.PluginInfo {
Expand Down
11 changes: 11 additions & 0 deletions scenario/testdata/skip-if.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: skip-if
description: a scenario with a skip-if condition
skip-if:
- foo: bar
# This causes the evaluation to succeed (expects name=bar when foo=bar)
name: bar
tests:
- foo: bar
# Normally this would cause the test to fail, but this will be skipped due
# to the skip-if above succeeding.
name: bizzy