Skip to content

Commit

Permalink
Merge pull request #33 from jaypipes/simplify-eval
Browse files Browse the repository at this point in the history
simplify Evaluable interface and Scenario.Run
  • Loading branch information
jaypipes authored Jun 21, 2024
2 parents 67bf475 + 36b2569 commit a5e6bc6
Show file tree
Hide file tree
Showing 10 changed files with 272 additions and 185 deletions.
102 changes: 102 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,108 @@ test spec also contains these fields:
[execspec]: https://github.com/gdt-dev/gdt/blob/2791e11105fd3c36d1f11a7d111e089be7cdc84c/exec/spec.go#L11-L34
[pipeexpect]: https://github.com/gdt-dev/gdt/blob/2791e11105fd3c36d1f11a7d111e089be7cdc84c/exec/assertions.go#L15-L26

### Timeouts and retrying assertions

When evaluating assertions for a test spec, `gdt` inspects the test's
`timeout` value to determine how long to retry the `get` call and recheck
the assertions.

If a test's `timeout` is empty, `gdt` inspects the scenario's
`defaults.timeout` value. If both of those values are empty, `gdt` will look
for any default `timeout` value that the plugin uses.

If you're interested in seeing the individual results of `gdt`'s
assertion-checks for a single `get` call, you can use the `gdt.WithDebug()`
function, like this test function demonstrates:

file: `testdata/matches.yaml`:

```yaml
name: matches
description: create a deployment and check the matches condition succeeds
fixtures:
- kind
tests:
- name: create-deployment
kube:
create: testdata/manifests/nginx-deployment.yaml
- name: deployment-exists
kube:
get: deployments/nginx
assert:
matches:
spec:
replicas: 2
template:
metadata:
labels:
app: nginx
status:
readyReplicas: 2
- name: delete-deployment
kube:
delete: deployments/nginx
```

file: `matches_test.go`

```go
import (
"github.com/gdt-dev/gdt"
_ "github.com/gdt-dev/kube"
kindfix "github.com/gdt-dev/kube/fixture/kind"
)
func TestMatches(t *testing.T) {
fp := filepath.Join("testdata", "matches.yaml")
kfix := kindfix.New()
s, err := gdt.From(fp)
ctx := gdt.NewContext(gdt.WithDebug())
ctx = gdt.RegisterFixture(ctx, "kind", kfix)
s.Run(ctx, t)
}
```

Here's what running `go test -v matches_test.go` would look like:

```
$ go test -v matches_test.go
=== RUN TestMatches
=== RUN TestMatches/matches
=== RUN TestMatches/matches/create-deployment
=== RUN TestMatches/matches/deployment-exists
deployment-exists (try 1 after 1.303µs) ok: false, terminal: false
deployment-exists (try 1 after 1.303µs) failure: assertion failed: match field not equal: $.status.readyReplicas not present in subject
deployment-exists (try 2 after 595.62786ms) ok: false, terminal: false
deployment-exists (try 2 after 595.62786ms) failure: assertion failed: match field not equal: $.status.readyReplicas not present in subject
deployment-exists (try 3 after 1.020003807s) ok: false, terminal: false
deployment-exists (try 3 after 1.020003807s) failure: assertion failed: match field not equal: $.status.readyReplicas not present in subject
deployment-exists (try 4 after 1.760006109s) ok: false, terminal: false
deployment-exists (try 4 after 1.760006109s) failure: assertion failed: match field not equal: $.status.readyReplicas had different values. expected 2 but found 1
deployment-exists (try 5 after 2.772416449s) ok: true, terminal: false
=== RUN TestMatches/matches/delete-deployment
--- PASS: TestMatches (3.32s)
--- PASS: TestMatches/matches (3.30s)
--- PASS: TestMatches/matches/create-deployment (0.01s)
--- PASS: TestMatches/matches/deployment-exists (2.78s)
--- PASS: TestMatches/matches/delete-deployment (0.02s)
PASS
ok command-line-arguments 3.683s
```

You can see from the debug output above that `gdt` created the Deployment and
then did a `kube.get` for the `deployments/nginx` Deployment. Initially
(attempt 1), the `assert.matches` assertion failed because the
`status.readyReplicas` field was not present in the returned resource. `gdt`
retried the `kube.get` call 4 more times (attempts 2-5), with attempts 2 and 3
failed the existence check for the `status.readyReplicas` field and attempt 4
failing the *value* check for the `status.readyReplicas` field being `1`
instead of the expected `2`. Finally, when the Deployment was completely rolled
out, attempt 5 succeeded in all the `assert.matches` assertions.

## Contributing and acknowledgements

`gdt` was inspired by [Gabbi](https://github.com/cdent/gabbi), the excellent
Expand Down
4 changes: 2 additions & 2 deletions context/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ func (s *fooSpec) UnmarshalYAML(node *yaml.Node) error {
return nil
}

func (s *fooSpec) Eval(ctx context.Context, t *testing.T) *result.Result {
return nil
func (s *fooSpec) Eval(ctx context.Context) (*result.Result, error) {
return nil, nil
}

type fooPlugin struct{}
Expand Down
2 changes: 0 additions & 2 deletions plugin/exec/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"bytes"
"context"
"os/exec"
"testing"

gdtcontext "github.com/gdt-dev/gdt/context"
"github.com/gdt-dev/gdt/debug"
Expand Down Expand Up @@ -38,7 +37,6 @@ type Action struct {
// respectively.
func (a *Action) Do(
ctx context.Context,
t *testing.T,
outbuf *bytes.Buffer,
errbuf *bytes.Buffer,
exitcode *int,
Expand Down
17 changes: 10 additions & 7 deletions plugin/exec/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ package exec
import (
"bytes"
"context"
"testing"

"github.com/gdt-dev/gdt/debug"
gdterrors "github.com/gdt-dev/gdt/errors"
Expand All @@ -17,30 +16,34 @@ import (
// Eval performs an action and evaluates the results of that action, returning
// a Result that informs the Scenario about what failed or succeeded about the
// Evaluable's conditions.
func (s *Spec) Eval(ctx context.Context, t *testing.T) *result.Result {
//
// Errors returned by Eval() are **RuntimeErrors**, not failures in assertions.
func (s *Spec) Eval(
ctx context.Context,
) (*result.Result, error) {
outbuf := &bytes.Buffer{}
errbuf := &bytes.Buffer{}

var ec int

if err := s.Do(ctx, t, outbuf, errbuf, &ec); err != nil {
if err := s.Do(ctx, outbuf, errbuf, &ec); err != nil {
if err == gdterrors.ErrTimeoutExceeded {
return result.New(result.WithFailures(gdterrors.ErrTimeoutExceeded))
return result.New(result.WithFailures(gdterrors.ErrTimeoutExceeded)), nil
}
return result.New(result.WithRuntimeError(ExecRuntimeError(err)))
return nil, ExecRuntimeError(err)
}
a := newAssertions(s.Assert, ec, outbuf, errbuf)
if !a.OK(ctx) {
if s.On != nil {
if s.On.Fail != nil {
outbuf.Reset()
errbuf.Reset()
err := s.On.Fail.Do(ctx, t, outbuf, errbuf, nil)
err := s.On.Fail.Do(ctx, outbuf, errbuf, nil)
if err != nil {
debug.Println(ctx, "error in on.fail.exec: %s", err)
}
}
}
}
return result.New(result.WithFailures(a.Failures()...))
return result.New(result.WithFailures(a.Failures()...)), nil
}
4 changes: 2 additions & 2 deletions plugin/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ func (s *fooSpec) Base() *gdttypes.Spec {
return &s.Spec
}

func (s *fooSpec) Eval(context.Context, *testing.T) *result.Result {
return nil
func (s *fooSpec) Eval(context.Context) (*result.Result, error) {
return nil, nil
}

func (s *fooSpec) UnmarshalYAML(node *yaml.Node) error {
Expand Down
34 changes: 0 additions & 34 deletions result/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,6 @@

package result

import (
"errors"
"fmt"

gdterrors "github.com/gdt-dev/gdt/errors"
)

// Result is returned from a `Evaluable.Eval` execution. It serves two
// purposes:
//
Expand All @@ -23,9 +16,6 @@ import (
// returned in the Result and the `Scenario.Run` method injects that
// information into the context that is supplied to the next Spec's `Run`.
type Result struct {
// err is any error that was returned from the Evaluable's execution. This
// is guaranteed to be a `gdterrors.RuntimeError`.
err error
// failures is the collection of error messages from assertion failures
// that occurred during Eval(). These are *not* `gdterrors.RuntimeError`.
failures []error
Expand All @@ -36,17 +26,6 @@ type Result struct {
data map[string]interface{}
}

// HasRuntimeError returns true if the Eval() returned a runtime error, false
// otherwise.
func (r *Result) HasRuntimeError() bool {
return r.err != nil
}

// RuntimeError returns the runtime error
func (r *Result) RuntimeError() error {
return r.err
}

// HasData returns true if any of the run data has been set, false otherwise.
func (r *Result) HasData() bool {
return r.data != nil
Expand Down Expand Up @@ -86,19 +65,6 @@ func (r *Result) SetFailures(failures ...error) {

type ResultModifier func(*Result)

// WithRuntimeError modifies the Result with the supplied error
func WithRuntimeError(err error) ResultModifier {
if !errors.Is(err, gdterrors.RuntimeError) {
msg := fmt.Sprintf("expected %s to be a gdterrors.RuntimeError", err)
// panic here because a plugin author incorrectly implemented their
// plugin Spec's Eval() method...
panic(msg)
}
return func(r *Result) {
r.err = err
}
}

// WithData modifies the Result with the supplied run data key and value
func WithData(key string, val interface{}) ResultModifier {
return func(r *Result) {
Expand Down
Loading

0 comments on commit a5e6bc6

Please sign in to comment.