Skip to content
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
19 changes: 13 additions & 6 deletions .github/workflows/fmtcheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,18 @@ jobs:
objects.githubusercontent.com:443
proxy.golang.org:443
blob.core.windows.net:443
- name: checkout code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: setup go
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version: 1.21

- name: checkout-code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0

- name: setup-go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: 'go.mod'
check-latest: true
cache: true

- name: check fmt
run: 'bash -c "diff -u <(echo -n) <(gofmt -d .)"'
23 changes: 15 additions & 8 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,22 @@ jobs:
with:
egress-policy: audit
disable-sudo: true
- name: checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: setup go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: 1.24

- name: checkout-code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0

- name: setup-go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: 'go.mod'
check-latest: true
cache: true

- name: lint
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
with:
version: v2.2.0
version: v2.15.0
args: --timeout=5m0s --verbose
only-new-issues: true
22 changes: 14 additions & 8 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
test:
strategy:
matrix:
go: ['1.21', '1.22', '1.23']
go: ['1.23', '1.24', '1.25']
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
Expand All @@ -30,11 +30,17 @@ jobs:
objects.githubusercontent.com:443
proxy.golang.org:443
blob.core.windows.net:443
- name: checkout code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: setup go
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version: ${{ matrix.go }}
check-latest: true

- name: checkout-code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0

- name: setup-go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: ${{ matrix.go }}
check-latest: true
cache: true

- run: go test -timeout 10s -v ./...
43 changes: 18 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,38 +153,31 @@ test spec also contains these fields:
`stderr` and `returncode` refer to the corresponding stdout, stderr
and return/exitcode values. All other string values for `var.from` indicate
the name of the environment variable to read into the named variable.
* `assert`: (optional) an object describing the conditions that will be
asserted about the test action.
* `assert.require`: (optional) a boolean indicating whether a failed assertion
will cause the test scenario's execution to stop. The default behaviour of
`gdt` is to continue execution of subsequent test specs in a test scenario when
an assertion fails.
* `assert`: (optional) object containing assertions to make about the action
performed by the test. Failed assertions will not cause the test scenario's
execution to stop. To have failed assertions stop the test scenario's
execution, use the `require` object.
* `assert.exit-code`: (optional) an integer with the expected exit code from the
executed command. The default successful exit code is 0 and therefore you do
not need to specify this if you expect a successful exit code.
* `assert.out`: (optional) a [`PipeExpect`][pipeexpect] object containing
* `assert.out`: (optional) an [`ExpectContains`][expectcontains] object containing
assertions about content in `stdout`.
* `assert.out.is`: (optional) a string with the exact contents of `stdout` you expect
to get.
* `assert.out.all`: (optional) a string or list of strings that *all* must be
present in `stdout`.
* `assert.out.any`: (optional) a string or list of strings of which *at
least one* must be present in `stdout`.
* `assert.out.none`: (optional) a string or list of strings of which *none
should be present* in `stdout`.
* `assert.err`: (optional) a [`PipeAssertions`][pipeexpect] object containing
* `assert.err`: (optional) an [`ExpectContains`][expectcontains] object containing
assertions about content in `stderr`.
* `require`: (optional) object containing assertions to make about the action
performed by the test. Failed assertions will cause the test scenario's
execution to stop. To have failed assertions not stop the test scenario's
execution, use the `assert` object.
* `require.exit-code`: (optional) an integer with the expected exit code from the
executed command. The default successful exit code is 0 and therefore you do
not need to specify this if you expect a successful exit code.
* `require.out`: (optional) an [`ExpectContains`][expectcontains] object containing
assertions about content in `stdout`.
* `require.err`: (optional) an [`ExpectContains`][expectcontains] object containing
assertions about content in `stderr`.
* `assert.err.is`: (optional) a string with the exact contents of `stderr` you expect
to get.
* `assert.err.all`: (optional) a string or list of strings that *all* must be
present in `stderr`.
* `assert.err.any`: (optional) a string or list of strings of which *at
least one* must be present in `stderr`.
* `assert.err.none`: (optional) a string or list of strings of which *none
should be present* in `stderr`.

[execspec]: https://github.com/gdt-dev/core/blob/2791e11105fd3c36d1f11a7d111e089be7cdc84c/exec/spec.go#L11-L34
[pipeexpect]: https://github.com/gdt-dev/core/blob/2791e11105fd3c36d1f11a7d111e089be7cdc84c/exec/assertions.go#L15-L26
[expectcontains]: https://github.com/gdt-dev/core/blob/main/api/expect.go

### Marking a program or package dependency for a test scenario

Expand Down
19 changes: 19 additions & 0 deletions api/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ var (
// ErrUnexpectedError is an ErrFailure when an unexpected error has
// occurred.
ErrUnexpectedError = fmt.Errorf("%w: unexpected error", ErrFailure)
// ErrExpectedError is an ErrFailure when an expected error did not occur.
ErrExpectedError = fmt.Errorf("%w: unexpected error", ErrFailure)
)

// TimeoutExceeded returns an ErrTimeoutExceeded when a test's execution
Expand Down Expand Up @@ -98,6 +100,23 @@ func UnexpectedError(err error) error {
return fmt.Errorf("%w: %s", ErrUnexpectedError, err)
}

// ExpectedError returns an ErrExpectedError when the supplied error did not
// occur or the supplied ExpectContains did not match.
func ExpectedError(subject any) error {
switch subject := subject.(type) {
case error:
return fmt.Errorf("%w: %s", ErrExpectedError, subject)
case ExpectContains:
expectMatch := subject.String()
return fmt.Errorf("%w: %s", ErrExpectedError, expectMatch)
default:
msg := fmt.Sprintf(
"unhandled subject type %T supplied to ExpectedError", subject,
)
panic(msg)
}
}

var (
// ErrUnknownSourceType indicates that a From() function was called with an
// unknown source parameter type.
Expand Down
88 changes: 88 additions & 0 deletions api/expect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package api

import (
"fmt"
"strings"

"gopkg.in/yaml.v3"

"github.com/gdt-dev/core/parse"
)

// ExpectContains is a common struct for assertions about strings or
// collections of strings.
type ExpectContains struct {
// ContainsAll is one or more strings that *all* must be present in the
// subject contents
ContainsAll *FlexStrings `yaml:"contains,omitempty"`
// ContainsNone is one or more strings, *none of which* should be present in
// the subject contents
ContainsNone *FlexStrings `yaml:"contains-none-of,omitempty"`
// ContainsOneOf is one or more strings of which *at least one* must be
// present in the subject contents
ContainsAny *FlexStrings `yaml:"contains-one-of,omitempty"`
}

// String returns a string representation of the ExpectContains' matching
// conditions.
func (e ExpectContains) String() string {
parts := []string{}
if e.ContainsAll != nil {
parts = append(parts, fmt.Sprintf("contains-all: %s", e.ContainsAll))
}
if e.ContainsNone != nil {
parts = append(parts, fmt.Sprintf("contains-none: %s", e.ContainsNone))
}
if e.ContainsAny != nil {
parts = append(parts, fmt.Sprintf("contains-any: %s", e.ContainsAny))
}
return strings.Join(parts, " ")
}

func (e *ExpectContains) UnmarshalYAML(node *yaml.Node) error {
if node.Kind != yaml.MappingNode {
return parse.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 parse.ExpectedScalarAt(keyNode)
}
key := keyNode.Value
valNode := node.Content[i+1]
switch key {
case "all", "is", "contains", "contains-all", "contains_all":
if valNode.Kind != yaml.ScalarNode && valNode.Kind != yaml.SequenceNode {
return parse.ExpectedScalarOrSequenceAt(valNode)
}
var v FlexStrings
if err := valNode.Decode(&v); err != nil {
return err
}
e.ContainsAll = &v
case "any", "contains-one-of", "contains-any", "contains_one_of", "contains_any":
if valNode.Kind != yaml.ScalarNode && valNode.Kind != yaml.SequenceNode {
return parse.ExpectedScalarOrSequenceAt(valNode)
}
var v FlexStrings
if err := valNode.Decode(&v); err != nil {
return err
}
e.ContainsAny = &v
case "none", "none-of", "contains-none-of", "contains-none", "none_of", "contains_none_of", "contains_none":
if valNode.Kind != yaml.ScalarNode && valNode.Kind != yaml.SequenceNode {
return parse.ExpectedScalarOrSequenceAt(valNode)
}
var v FlexStrings
if err := valNode.Decode(&v); err != nil {
return err
}
e.ContainsNone = &v
default:
return parse.UnknownFieldAt(key, keyNode)
}
}
return nil
}
19 changes: 19 additions & 0 deletions api/flexstrings.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
package api

import (
"strings"

"gopkg.in/yaml.v3"

"github.com/gdt-dev/core/parse"
Expand All @@ -21,6 +23,23 @@ func (f *FlexStrings) Values() []string {
return f.values
}

// NewFlexStrings returns a new FlexStrings from zero or more string args.
func NewFlexStrings(args ...string) *FlexStrings {
return &FlexStrings{values: args}
}

// String returns a string representation of the FlexStrings
func (f FlexStrings) String() string {
switch len(f.values) {
case 0:
return ""
case 1:
return f.values[0]
default:
return "[" + strings.Join(f.values, ",") + "]"
}
}

// UnmarshalYAML is a custom unmarshaler that understands that the value of the
// FlexStrings can be either a string or a slice of strings.
func (f *FlexStrings) UnmarshalYAML(node *yaml.Node) error {
Expand Down
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.6.0
github.com/samber/lo v1.51.0
github.com/samber/lo v1.52.0
github.com/stretchr/testify v1.11.1
github.com/theory/jsonpath v0.10.1
github.com/xeipuuv/gojsonschema v1.2.0
Expand All @@ -16,9 +16,10 @@ require (

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/text v0.23.0 // indirect
)
18 changes: 10 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
Expand All @@ -32,8 +34,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
Expand Down
Loading