Merge pull request #886 from s1061123/update-libcni

Bump libcni to fetch the bugfix
This commit is contained in:
Doug Smith 2022-07-29 11:06:14 -04:00 committed by GitHub
commit fba6d66720
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 591 additions and 61 deletions

View File

@ -16,11 +16,10 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Run Revive - name: Run Revive Action by pulling pre-built image
run: | uses: docker://morphy/revive-action:v2
GO111MODULE=off go get github.com/mgechev/revive with:
$(go env GOPATH)/bin/revive -exclude ./vendor/... ./... # this is ouput for user exclude: "./vendor/..."
$(go env GOPATH)/bin/revive -exclude ./vendor/... ./...| xargs -0 -r false # this is for github actions
- name: Run go fmt - name: Run go fmt
run: go fmt ./... run: go fmt ./...

4
go.mod
View File

@ -4,14 +4,14 @@ go 1.17
require ( require (
github.com/blang/semver v3.5.1+incompatible github.com/blang/semver v3.5.1+incompatible
github.com/containernetworking/cni v1.0.1 github.com/containernetworking/cni v1.1.2
github.com/containernetworking/plugins v1.1.0 github.com/containernetworking/plugins v1.1.0
github.com/fsnotify/fsnotify v1.5.1 github.com/fsnotify/fsnotify v1.5.1
github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/logr v1.2.3 // indirect
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.3.0 github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.3.0
github.com/onsi/ginkgo v1.16.5 github.com/onsi/ginkgo v1.16.5
github.com/onsi/gomega v1.15.0 github.com/onsi/gomega v1.17.0
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5 github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4

7
go.sum
View File

@ -231,6 +231,8 @@ github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ
github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
github.com/containernetworking/cni v1.0.1 h1:9OIL/sZmMYDBe+G8svzILAlulUpaDTUjeAbtH/JNLBo= github.com/containernetworking/cni v1.0.1 h1:9OIL/sZmMYDBe+G8svzILAlulUpaDTUjeAbtH/JNLBo=
github.com/containernetworking/cni v1.0.1/go.mod h1:AKuhXbN5EzmD4yTNtfSsX3tPcmtrBI6QcRV0NiNt15Y= github.com/containernetworking/cni v1.0.1/go.mod h1:AKuhXbN5EzmD4yTNtfSsX3tPcmtrBI6QcRV0NiNt15Y=
github.com/containernetworking/cni v1.1.2 h1:wtRGZVv7olUHMOqouPpn3cXJWpJgM6+EUl31EQbXALQ=
github.com/containernetworking/cni v1.1.2/go.mod h1:sDpYKmGVENF3s6uvMvGgldDWeG8dMxakj/u+i9ht9vw=
github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM=
github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8= github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8=
github.com/containernetworking/plugins v1.1.0 h1:kTIldaDo9SlbQsjhUKvDx0v9q7zyIFJH/Rm9F4xRBro= github.com/containernetworking/plugins v1.1.0 h1:kTIldaDo9SlbQsjhUKvDx0v9q7zyIFJH/Rm9F4xRBro=
@ -419,6 +421,7 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -471,6 +474,7 @@ github.com/heketi/heketi v10.3.0+incompatible/go.mod h1:bB9ly3RchcQqsQ9CpyaQwvva
github.com/heketi/tests v0.0.0-20151005000721-f3775cbcefd6/go.mod h1:xGMAM8JLi7UkZt1i4FQeQy0R2T8GLUwQhOP5M1gBhy4= github.com/heketi/tests v0.0.0-20151005000721-f3775cbcefd6/go.mod h1:xGMAM8JLi7UkZt1i4FQeQy0R2T8GLUwQhOP5M1gBhy4=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
@ -595,6 +599,7 @@ github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9k
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
@ -603,6 +608,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU= github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU=
github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0=
github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=

View File

@ -21,6 +21,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
"github.com/containernetworking/cni/pkg/types"
) )
type NotFoundError struct { type NotFoundError struct {
@ -41,8 +43,8 @@ func (e NoConfigsFoundError) Error() string {
} }
func ConfFromBytes(bytes []byte) (*NetworkConfig, error) { func ConfFromBytes(bytes []byte) (*NetworkConfig, error) {
conf := &NetworkConfig{Bytes: bytes} conf := &NetworkConfig{Bytes: bytes, Network: &types.NetConf{}}
if err := json.Unmarshal(bytes, &conf.Network); err != nil { if err := json.Unmarshal(bytes, conf.Network); err != nil {
return nil, fmt.Errorf("error parsing configuration: %w", err) return nil, fmt.Errorf("error parsing configuration: %w", err)
} }
if conf.Network.Type == "" { if conf.Network.Type == "" {

View File

@ -16,6 +16,7 @@ package invoke
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"os" "os"
@ -33,6 +34,49 @@ type Exec interface {
Decode(jsonBytes []byte) (version.PluginInfo, error) Decode(jsonBytes []byte) (version.PluginInfo, error)
} }
// Plugin must return result in same version as specified in netconf; but
// for backwards compatibility reasons if the result version is empty use
// config version (rather than technically correct 0.1.0).
// https://github.com/containernetworking/cni/issues/895
func fixupResultVersion(netconf, result []byte) (string, []byte, error) {
versionDecoder := &version.ConfigDecoder{}
confVersion, err := versionDecoder.Decode(netconf)
if err != nil {
return "", nil, err
}
var rawResult map[string]interface{}
if err := json.Unmarshal(result, &rawResult); err != nil {
return "", nil, fmt.Errorf("failed to unmarshal raw result: %w", err)
}
// plugin output of "null" is successfully unmarshalled, but results in a nil
// map which causes a panic when the confVersion is assigned below.
if rawResult == nil {
rawResult = make(map[string]interface{})
}
// Manually decode Result version; we need to know whether its cniVersion
// is empty, while built-in decoders (correctly) substitute 0.1.0 for an
// empty version per the CNI spec.
if resultVerRaw, ok := rawResult["cniVersion"]; ok {
resultVer, ok := resultVerRaw.(string)
if ok && resultVer != "" {
return resultVer, result, nil
}
}
// If the cniVersion is not present or empty, assume the result is
// the same CNI spec version as the config
rawResult["cniVersion"] = confVersion
newBytes, err := json.Marshal(rawResult)
if err != nil {
return "", nil, fmt.Errorf("failed to remarshal fixed result: %w", err)
}
return confVersion, newBytes, nil
}
// For example, a testcase could pass an instance of the following fakeExec // For example, a testcase could pass an instance of the following fakeExec
// object to ExecPluginWithResult() to verify the incoming stdin and environment // object to ExecPluginWithResult() to verify the incoming stdin and environment
// and provide a tailored response: // and provide a tailored response:
@ -84,7 +128,12 @@ func ExecPluginWithResult(ctx context.Context, pluginPath string, netconf []byte
return nil, err return nil, err
} }
return create.CreateFromBytes(stdoutBytes) resultVersion, fixedBytes, err := fixupResultVersion(netconf, stdoutBytes)
if err != nil {
return nil, err
}
return create.Create(resultVersion, fixedBytes)
} }
func ExecPluginWithoutResult(ctx context.Context, pluginPath string, netconf []byte, args CNIArgs, exec Exec) error { func ExecPluginWithoutResult(ctx context.Context, pluginPath string, netconf []byte, args CNIArgs, exec Exec) error {

View File

@ -196,6 +196,7 @@ func (t *dispatcher) pluginMain(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error,
// Print the about string to stderr when no command is set // Print the about string to stderr when no command is set
if err.Code == types.ErrInvalidEnvironmentVariables && t.Getenv("CNI_COMMAND") == "" && about != "" { if err.Code == types.ErrInvalidEnvironmentVariables && t.Getenv("CNI_COMMAND") == "" && about != "" {
_, _ = fmt.Fprintln(t.Stderr, about) _, _ = fmt.Fprintln(t.Stderr, about)
_, _ = fmt.Fprintf(t.Stderr, "CNI protocol versions supported: %s\n", strings.Join(versionInfo.SupportedVersions(), ", "))
return nil return nil
} }
return err return err
@ -248,10 +249,7 @@ func (t *dispatcher) pluginMain(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error,
return types.NewError(types.ErrInvalidEnvironmentVariables, fmt.Sprintf("unknown CNI_COMMAND: %v", cmd), "") return types.NewError(types.ErrInvalidEnvironmentVariables, fmt.Sprintf("unknown CNI_COMMAND: %v", cmd), "")
} }
if err != nil {
return err return err
}
return nil
} }
// PluginMainWithError is the core "main" for a plugin. It accepts // PluginMainWithError is the core "main" for a plugin. It accepts

View File

@ -86,8 +86,8 @@ func (*PluginDecoder) Decode(jsonBytes []byte) (PluginInfo, error) {
// minor, and micro numbers or returns an error // minor, and micro numbers or returns an error
func ParseVersion(version string) (int, int, int, error) { func ParseVersion(version string) (int, int, int, error) {
var major, minor, micro int var major, minor, micro int
if version == "" { if version == "" { // special case: no version declared == v0.1.0
return -1, -1, -1, fmt.Errorf("invalid version %q: the version is empty", version) return 0, 1, 0, nil
} }
parts := strings.Split(version, ".") parts := strings.Split(version, ".")

View File

@ -1,3 +1,23 @@
## 1.17.0
### Features
- Add HaveField matcher [3a26311]
- add Error() assertions on the final error value of multi-return values (#480) [2f96943]
- separate out offsets and timeouts (#478) [18a4723]
- fix transformation error reporting (#479) [e001fab]
- allow transform functions to report errors (#472) [bf93408]
### Fixes
Stop using deprecated ioutil package (#467) [07f405d]
## 1.16.0
### Features
- feat: HaveHTTPStatus multiple expected values (#465) [aa69f1b]
- feat: HaveHTTPHeaderWithValue() matcher (#463) [dd83a96]
- feat: HaveHTTPBody matcher (#462) [504e1f2]
- feat: formatter for HTTP responses (#461) [e5b3157]
## 1.15.0 ## 1.15.0
### Fixes ### Fixes

View File

@ -22,7 +22,7 @@ import (
"github.com/onsi/gomega/types" "github.com/onsi/gomega/types"
) )
const GOMEGA_VERSION = "1.15.0" const GOMEGA_VERSION = "1.17.0"
const nilGomegaPanic = `You are trying to make an assertion, but haven't registered Gomega's fail handler. const nilGomegaPanic = `You are trying to make an assertion, but haven't registered Gomega's fail handler.
If you're using Ginkgo then you probably forgot to put your assertion in an It(). If you're using Ginkgo then you probably forgot to put your assertion in an It().
@ -204,7 +204,8 @@ func Expect(actual interface{}, extra ...interface{}) Assertion {
// ExpectWithOffset(1, "foo").To(Equal("foo")) // ExpectWithOffset(1, "foo").To(Equal("foo"))
// //
// Unlike `Expect` and `Ω`, `ExpectWithOffset` takes an additional integer argument // Unlike `Expect` and `Ω`, `ExpectWithOffset` takes an additional integer argument
// that is used to modify the call-stack offset when computing line numbers. // that is used to modify the call-stack offset when computing line numbers. It is
// the same as `Expect(...).WithOffset`.
// //
// This is most useful in helper functions that make assertions. If you want Gomega's // This is most useful in helper functions that make assertions. If you want Gomega's
// error message to refer to the calling line in the test (as opposed to the line in the helper function) // error message to refer to the calling line in the test (as opposed to the line in the helper function)
@ -300,6 +301,9 @@ For example:
}).Should(Succeed()) }).Should(Succeed())
will rerun the function until all assertions pass. will rerun the function until all assertions pass.
`Eventually` specifying a timeout interval (and an optional polling interval) are
the same as `Eventually(...).WithTimeout` or `Eventually(...).WithTimeout(...).WithPolling`.
*/ */
func Eventually(actual interface{}, intervals ...interface{}) AsyncAssertion { func Eventually(actual interface{}, intervals ...interface{}) AsyncAssertion {
ensureDefaultGomegaIsConfigured() ensureDefaultGomegaIsConfigured()
@ -309,6 +313,12 @@ func Eventually(actual interface{}, intervals ...interface{}) AsyncAssertion {
// EventuallyWithOffset operates like Eventually but takes an additional // EventuallyWithOffset operates like Eventually but takes an additional
// initial argument to indicate an offset in the call stack. This is useful when building helper // initial argument to indicate an offset in the call stack. This is useful when building helper
// functions that contain matchers. To learn more, read about `ExpectWithOffset`. // functions that contain matchers. To learn more, read about `ExpectWithOffset`.
//
// `EventuallyWithOffset` is the same as `Eventually(...).WithOffset`.
//
// `EventuallyWithOffset` specifying a timeout interval (and an optional polling interval) are
// the same as `Eventually(...).WithOffset(...).WithTimeout` or
// `Eventually(...).WithOffset(...).WithTimeout(...).WithPolling`.
func EventuallyWithOffset(offset int, actual interface{}, intervals ...interface{}) AsyncAssertion { func EventuallyWithOffset(offset int, actual interface{}, intervals ...interface{}) AsyncAssertion {
ensureDefaultGomegaIsConfigured() ensureDefaultGomegaIsConfigured()
return Default.EventuallyWithOffset(offset, actual, intervals...) return Default.EventuallyWithOffset(offset, actual, intervals...)
@ -337,6 +347,9 @@ func Consistently(actual interface{}, intervals ...interface{}) AsyncAssertion {
// ConsistentlyWithOffset operates like Consistently but takes an additional // ConsistentlyWithOffset operates like Consistently but takes an additional
// initial argument to indicate an offset in the call stack. This is useful when building helper // initial argument to indicate an offset in the call stack. This is useful when building helper
// functions that contain matchers. To learn more, read about `ExpectWithOffset`. // functions that contain matchers. To learn more, read about `ExpectWithOffset`.
//
// `ConsistentlyWithOffset` is the same as `Consistently(...).WithOffset` and
// optional `WithTimeout` and `WithPolling`.
func ConsistentlyWithOffset(offset int, actual interface{}, intervals ...interface{}) AsyncAssertion { func ConsistentlyWithOffset(offset int, actual interface{}, intervals ...interface{}) AsyncAssertion {
ensureDefaultGomegaIsConfigured() ensureDefaultGomegaIsConfigured()
return Default.ConsistentlyWithOffset(offset, actual, intervals...) return Default.ConsistentlyWithOffset(offset, actual, intervals...)

View File

@ -8,44 +8,64 @@ import (
) )
type Assertion struct { type Assertion struct {
actualInput interface{} actuals []interface{} // actual value plus all extra values
actualIndex int // value to pass to the matcher
vet vetinari // the vet to call before calling Gomega matcher
offset int offset int
extra []interface{}
g *Gomega g *Gomega
} }
// ...obligatory discworld reference, as "vetineer" doesn't sound ... quite right.
type vetinari func(assertion *Assertion, optionalDescription ...interface{}) bool
func NewAssertion(actualInput interface{}, g *Gomega, offset int, extra ...interface{}) *Assertion { func NewAssertion(actualInput interface{}, g *Gomega, offset int, extra ...interface{}) *Assertion {
return &Assertion{ return &Assertion{
actualInput: actualInput, actuals: append([]interface{}{actualInput}, extra...),
actualIndex: 0,
vet: (*Assertion).vetActuals,
offset: offset, offset: offset,
extra: extra,
g: g, g: g,
} }
} }
func (assertion *Assertion) WithOffset(offset int) types.Assertion {
assertion.offset = offset
return assertion
}
func (assertion *Assertion) Error() types.Assertion {
return &Assertion{
actuals: assertion.actuals,
actualIndex: len(assertion.actuals) - 1,
vet: (*Assertion).vetError,
offset: assertion.offset,
g: assertion.g,
}
}
func (assertion *Assertion) Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { func (assertion *Assertion) Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
assertion.g.THelper() assertion.g.THelper()
return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, true, optionalDescription...) return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, true, optionalDescription...)
} }
func (assertion *Assertion) ShouldNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { func (assertion *Assertion) ShouldNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
assertion.g.THelper() assertion.g.THelper()
return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, false, optionalDescription...) return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, false, optionalDescription...)
} }
func (assertion *Assertion) To(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { func (assertion *Assertion) To(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
assertion.g.THelper() assertion.g.THelper()
return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, true, optionalDescription...) return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, true, optionalDescription...)
} }
func (assertion *Assertion) ToNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { func (assertion *Assertion) ToNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
assertion.g.THelper() assertion.g.THelper()
return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, false, optionalDescription...) return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, false, optionalDescription...)
} }
func (assertion *Assertion) NotTo(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { func (assertion *Assertion) NotTo(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
assertion.g.THelper() assertion.g.THelper()
return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, false, optionalDescription...) return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, false, optionalDescription...)
} }
func (assertion *Assertion) buildDescription(optionalDescription ...interface{}) string { func (assertion *Assertion) buildDescription(optionalDescription ...interface{}) string {
@ -61,7 +81,8 @@ func (assertion *Assertion) buildDescription(optionalDescription ...interface{})
} }
func (assertion *Assertion) match(matcher types.GomegaMatcher, desiredMatch bool, optionalDescription ...interface{}) bool { func (assertion *Assertion) match(matcher types.GomegaMatcher, desiredMatch bool, optionalDescription ...interface{}) bool {
matches, err := matcher.Match(assertion.actualInput) actualInput := assertion.actuals[assertion.actualIndex]
matches, err := matcher.Match(actualInput)
assertion.g.THelper() assertion.g.THelper()
if err != nil { if err != nil {
description := assertion.buildDescription(optionalDescription...) description := assertion.buildDescription(optionalDescription...)
@ -71,9 +92,9 @@ func (assertion *Assertion) match(matcher types.GomegaMatcher, desiredMatch bool
if matches != desiredMatch { if matches != desiredMatch {
var message string var message string
if desiredMatch { if desiredMatch {
message = matcher.FailureMessage(assertion.actualInput) message = matcher.FailureMessage(actualInput)
} else { } else {
message = matcher.NegatedFailureMessage(assertion.actualInput) message = matcher.NegatedFailureMessage(actualInput)
} }
description := assertion.buildDescription(optionalDescription...) description := assertion.buildDescription(optionalDescription...)
assertion.g.Fail(description+message, 2+assertion.offset) assertion.g.Fail(description+message, 2+assertion.offset)
@ -83,8 +104,11 @@ func (assertion *Assertion) match(matcher types.GomegaMatcher, desiredMatch bool
return true return true
} }
func (assertion *Assertion) vetExtras(optionalDescription ...interface{}) bool { // vetActuals vets the actual values, with the (optional) exception of a
success, message := vetExtras(assertion.extra) // specific value, such as the first value in case non-error assertions, or the
// last value in case of Error()-based assertions.
func (assertion *Assertion) vetActuals(optionalDescription ...interface{}) bool {
success, message := vetActuals(assertion.actuals, assertion.actualIndex)
if success { if success {
return true return true
} }
@ -95,12 +119,29 @@ func (assertion *Assertion) vetExtras(optionalDescription ...interface{}) bool {
return false return false
} }
func vetExtras(extras []interface{}) (bool, string) { // vetError vets the actual values, except for the final error value, in case
for i, extra := range extras { // the final error value is non-zero. Otherwise, it doesn't vet the actual
if extra != nil { // values, as these are allowed to take on any values unless there is a non-zero
zeroValue := reflect.Zero(reflect.TypeOf(extra)).Interface() // error value.
if !reflect.DeepEqual(zeroValue, extra) { func (assertion *Assertion) vetError(optionalDescription ...interface{}) bool {
message := fmt.Sprintf("Unexpected non-nil/non-zero extra argument at index %d:\n\t<%T>: %#v", i+1, extra, extra) if err := assertion.actuals[assertion.actualIndex]; err != nil {
// Go error result idiom: all other actual values must be zero values.
return assertion.vetActuals(optionalDescription...)
}
return true
}
// vetActuals vets a slice of actual values, optionally skipping a particular
// value slice element, such as the first or last value slice element.
func vetActuals(actuals []interface{}, skipIndex int) (bool, string) {
for i, actual := range actuals {
if i == skipIndex {
continue
}
if actual != nil {
zeroValue := reflect.Zero(reflect.TypeOf(actual)).Interface()
if !reflect.DeepEqual(zeroValue, actual) {
message := fmt.Sprintf("Unexpected non-nil/non-zero argument at index %d:\n\t<%T>: %#v", i, actual, actual)
return false, message return false, message
} }
} }

View File

@ -87,6 +87,21 @@ func NewAsyncAssertion(asyncType AsyncAssertionType, actualInput interface{}, g
return out return out
} }
func (assertion *AsyncAssertion) WithOffset(offset int) types.AsyncAssertion {
assertion.offset = offset
return assertion
}
func (assertion *AsyncAssertion) WithTimeout(interval time.Duration) types.AsyncAssertion {
assertion.timeoutInterval = interval
return assertion
}
func (assertion *AsyncAssertion) WithPolling(interval time.Duration) types.AsyncAssertion {
assertion.pollingInterval = interval
return assertion
}
func (assertion *AsyncAssertion) Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { func (assertion *AsyncAssertion) Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
assertion.g.THelper() assertion.g.THelper()
return assertion.match(matcher, true, optionalDescription...) return assertion.match(matcher, true, optionalDescription...)
@ -118,11 +133,11 @@ func (assertion *AsyncAssertion) pollActual() (interface{}, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
extras := []interface{}{} extras := []interface{}{nil}
for _, value := range values[1:] { for _, value := range values[1:] {
extras = append(extras, value.Interface()) extras = append(extras, value.Interface())
} }
success, message := vetExtras(extras) success, message := vetActuals(extras, 0)
if !success { if !success {
return nil, errors.New(message) return nil, errors.New(message)
} }

View File

@ -39,12 +39,12 @@ func (g *Gomega) ConfigureWithT(t types.GomegaTestingT) *Gomega {
return g return g
} }
func (g *Gomega) Ω(atual interface{}, extra ...interface{}) types.Assertion { func (g *Gomega) Ω(actual interface{}, extra ...interface{}) types.Assertion {
return g.ExpectWithOffset(0, atual, extra...) return g.ExpectWithOffset(0, actual, extra...)
} }
func (g *Gomega) Expect(atual interface{}, extra ...interface{}) types.Assertion { func (g *Gomega) Expect(actual interface{}, extra ...interface{}) types.Assertion {
return g.ExpectWithOffset(0, atual, extra...) return g.ExpectWithOffset(0, actual, extra...)
} }
func (g *Gomega) ExpectWithOffset(offset int, actual interface{}, extra ...interface{}) types.Assertion { func (g *Gomega) ExpectWithOffset(offset int, actual interface{}, extra ...interface{}) types.Assertion {

View File

@ -342,6 +342,34 @@ func HaveKeyWithValue(key interface{}, value interface{}) types.GomegaMatcher {
} }
} }
//HaveField succeeds if actual is a struct and the value at the passed in field
//matches the passed in matcher. By default HaveField used Equal() to perform the match,
//however a matcher can be passed in in stead.
//
//The field must be a string that resolves to the name of a field in the struct. Structs can be traversed
//using the '.' delimiter. If the field ends with '()' a method named field is assumed to exist on the struct and is invoked.
//Such methods must take no arguments and return a single value:
//
// type Book struct {
// Title string
// Author Person
// }
// type Person struct {
// FirstName string
// LastName string
// DOB time.Time
// }
// Expect(book).To(HaveField("Title", "Les Miserables"))
// Expect(book).To(HaveField("Title", ContainSubstring("Les"))
// Expect(book).To(HaveField("Person.FirstName", Equal("Victor"))
// Expect(book).To(HaveField("Person.DOB.Year()", BeNumerically("<", 1900))
func HaveField(field string, expected interface{}) types.GomegaMatcher {
return &matchers.HaveFieldMatcher{
Field: field,
Expected: expected,
}
}
//BeNumerically performs numerical assertions in a type-agnostic way. //BeNumerically performs numerical assertions in a type-agnostic way.
//Actual and expected should be numbers, though the specific type of //Actual and expected should be numbers, though the specific type of
//number is irrelevant (float32, float64, uint8, etc...). //number is irrelevant (float32, float64, uint8, etc...).
@ -423,10 +451,29 @@ func BeADirectory() types.GomegaMatcher {
//Expected must be either an int or a string. //Expected must be either an int or a string.
// Expect(resp).Should(HaveHTTPStatus(http.StatusOK)) // asserts that resp.StatusCode == 200 // Expect(resp).Should(HaveHTTPStatus(http.StatusOK)) // asserts that resp.StatusCode == 200
// Expect(resp).Should(HaveHTTPStatus("404 Not Found")) // asserts that resp.Status == "404 Not Found" // Expect(resp).Should(HaveHTTPStatus("404 Not Found")) // asserts that resp.Status == "404 Not Found"
func HaveHTTPStatus(expected interface{}) types.GomegaMatcher { // Expect(resp).Should(HaveHTTPStatus(http.StatusOK, http.StatusNoContent)) // asserts that resp.StatusCode == 200 || resp.StatusCode == 204
func HaveHTTPStatus(expected ...interface{}) types.GomegaMatcher {
return &matchers.HaveHTTPStatusMatcher{Expected: expected} return &matchers.HaveHTTPStatusMatcher{Expected: expected}
} }
// HaveHTTPHeaderWithValue succeeds if the header is found and the value matches.
// Actual must be either a *http.Response or *httptest.ResponseRecorder.
// Expected must be a string header name, followed by a header value which
// can be a string, or another matcher.
func HaveHTTPHeaderWithValue(header string, value interface{}) types.GomegaMatcher {
return &matchers.HaveHTTPHeaderWithValueMatcher{
Header: header,
Value: value,
}
}
// HaveHTTPBody matches if the body matches.
// Actual must be either a *http.Response or *httptest.ResponseRecorder.
// Expected must be either a string, []byte, or other matcher
func HaveHTTPBody(expected interface{}) types.GomegaMatcher {
return &matchers.HaveHTTPBodyMatcher{Expected: expected}
}
//And succeeds only if all of the given matchers succeed. //And succeeds only if all of the given matchers succeed.
//The matchers are tried in order, and will fail-fast if one doesn't succeed. //The matchers are tried in order, and will fail-fast if one doesn't succeed.
// Expect("hi").To(And(HaveLen(2), Equal("hi")) // Expect("hi").To(And(HaveLen(2), Equal("hi"))
@ -466,10 +513,15 @@ func Not(matcher types.GomegaMatcher) types.GomegaMatcher {
} }
//WithTransform applies the `transform` to the actual value and matches it against `matcher`. //WithTransform applies the `transform` to the actual value and matches it against `matcher`.
//The given transform must be a function of one parameter that returns one value. //The given transform must be either a function of one parameter that returns one value or a
// function of one parameter that returns two values, where the second value must be of the
// error type.
// var plus1 = func(i int) int { return i + 1 } // var plus1 = func(i int) int { return i + 1 }
// Expect(1).To(WithTransform(plus1, Equal(2)) // Expect(1).To(WithTransform(plus1, Equal(2))
// //
// var failingplus1 = func(i int) (int, error) { return 42, "this does not compute" }
// Expect(1).To(WithTransform(failingplus1, Equal(2)))
//
//And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions. //And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions.
func WithTransform(transform interface{}, matcher types.GomegaMatcher) types.GomegaMatcher { func WithTransform(transform interface{}, matcher types.GomegaMatcher) types.GomegaMatcher {
return matchers.NewWithTransformMatcher(transform, matcher) return matchers.NewWithTransformMatcher(transform, matcher)

80
vendor/github.com/onsi/gomega/matchers/have_field.go generated vendored Normal file
View File

@ -0,0 +1,80 @@
package matchers
import (
"fmt"
"reflect"
"strings"
"github.com/onsi/gomega/format"
)
func extractField(actual interface{}, field string) (interface{}, error) {
fields := strings.SplitN(field, ".", 2)
actualValue := reflect.ValueOf(actual)
if actualValue.Kind() != reflect.Struct {
return nil, fmt.Errorf("HaveField encountered:\n%s\nWhich is not a struct.", format.Object(actual, 1))
}
var extractedValue reflect.Value
if strings.HasSuffix(fields[0], "()") {
extractedValue = actualValue.MethodByName(strings.TrimSuffix(fields[0], "()"))
if extractedValue == (reflect.Value{}) {
return nil, fmt.Errorf("HaveField could not find method named '%s' in struct of type %T.", fields[0], actual)
}
t := extractedValue.Type()
if t.NumIn() != 0 || t.NumOut() != 1 {
return nil, fmt.Errorf("HaveField found an invalid method named '%s' in struct of type %T.\nMethods must take no arguments and return exactly one value.", fields[0], actual)
}
extractedValue = extractedValue.Call([]reflect.Value{})[0]
} else {
extractedValue = actualValue.FieldByName(fields[0])
if extractedValue == (reflect.Value{}) {
return nil, fmt.Errorf("HaveField could not find field named '%s' in struct:\n%s", fields[0], format.Object(actual, 1))
}
}
if len(fields) == 1 {
return extractedValue.Interface(), nil
} else {
return extractField(extractedValue.Interface(), fields[1])
}
}
type HaveFieldMatcher struct {
Field string
Expected interface{}
extractedField interface{}
expectedMatcher omegaMatcher
}
func (matcher *HaveFieldMatcher) Match(actual interface{}) (success bool, err error) {
matcher.extractedField, err = extractField(actual, matcher.Field)
if err != nil {
return false, err
}
var isMatcher bool
matcher.expectedMatcher, isMatcher = matcher.Expected.(omegaMatcher)
if !isMatcher {
matcher.expectedMatcher = &EqualMatcher{Expected: matcher.Expected}
}
return matcher.expectedMatcher.Match(matcher.extractedField)
}
func (matcher *HaveFieldMatcher) FailureMessage(actual interface{}) (message string) {
message = fmt.Sprintf("Value for field '%s' failed to satisfy matcher.\n", matcher.Field)
message += matcher.expectedMatcher.FailureMessage(matcher.extractedField)
return message
}
func (matcher *HaveFieldMatcher) NegatedFailureMessage(actual interface{}) (message string) {
message = fmt.Sprintf("Value for field '%s' satisfied matcher, but should not have.\n", matcher.Field)
message += matcher.expectedMatcher.NegatedFailureMessage(matcher.extractedField)
return message
}

View File

@ -0,0 +1,101 @@
package matchers
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"github.com/onsi/gomega/format"
"github.com/onsi/gomega/types"
)
type HaveHTTPBodyMatcher struct {
Expected interface{}
cachedBody []byte
}
func (matcher *HaveHTTPBodyMatcher) Match(actual interface{}) (bool, error) {
body, err := matcher.body(actual)
if err != nil {
return false, err
}
switch e := matcher.Expected.(type) {
case string:
return (&EqualMatcher{Expected: e}).Match(string(body))
case []byte:
return (&EqualMatcher{Expected: e}).Match(body)
case types.GomegaMatcher:
return e.Match(body)
default:
return false, fmt.Errorf("HaveHTTPBody matcher expects string, []byte, or GomegaMatcher. Got:\n%s", format.Object(matcher.Expected, 1))
}
}
func (matcher *HaveHTTPBodyMatcher) FailureMessage(actual interface{}) (message string) {
body, err := matcher.body(actual)
if err != nil {
return fmt.Sprintf("failed to read body: %s", err)
}
switch e := matcher.Expected.(type) {
case string:
return (&EqualMatcher{Expected: e}).FailureMessage(string(body))
case []byte:
return (&EqualMatcher{Expected: e}).FailureMessage(body)
case types.GomegaMatcher:
return e.FailureMessage(body)
default:
return fmt.Sprintf("HaveHTTPBody matcher expects string, []byte, or GomegaMatcher. Got:\n%s", format.Object(matcher.Expected, 1))
}
}
func (matcher *HaveHTTPBodyMatcher) NegatedFailureMessage(actual interface{}) (message string) {
body, err := matcher.body(actual)
if err != nil {
return fmt.Sprintf("failed to read body: %s", err)
}
switch e := matcher.Expected.(type) {
case string:
return (&EqualMatcher{Expected: e}).NegatedFailureMessage(string(body))
case []byte:
return (&EqualMatcher{Expected: e}).NegatedFailureMessage(body)
case types.GomegaMatcher:
return e.NegatedFailureMessage(body)
default:
return fmt.Sprintf("HaveHTTPBody matcher expects string, []byte, or GomegaMatcher. Got:\n%s", format.Object(matcher.Expected, 1))
}
}
// body returns the body. It is cached because once we read it in Match()
// the Reader is closed and it is not readable again in FailureMessage()
// or NegatedFailureMessage()
func (matcher *HaveHTTPBodyMatcher) body(actual interface{}) ([]byte, error) {
if matcher.cachedBody != nil {
return matcher.cachedBody, nil
}
body := func(a *http.Response) ([]byte, error) {
if a.Body != nil {
defer a.Body.Close()
var err error
matcher.cachedBody, err = io.ReadAll(a.Body)
if err != nil {
return nil, fmt.Errorf("error reading response body: %w", err)
}
}
return matcher.cachedBody, nil
}
switch a := actual.(type) {
case *http.Response:
return body(a)
case *httptest.ResponseRecorder:
return body(a.Result())
default:
return nil, fmt.Errorf("HaveHTTPBody matcher expects *http.Response or *httptest.ResponseRecorder. Got:\n%s", format.Object(actual, 1))
}
}

View File

@ -0,0 +1,81 @@
package matchers
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/onsi/gomega/format"
"github.com/onsi/gomega/types"
)
type HaveHTTPHeaderWithValueMatcher struct {
Header string
Value interface{}
}
func (matcher *HaveHTTPHeaderWithValueMatcher) Match(actual interface{}) (success bool, err error) {
headerValue, err := matcher.extractHeader(actual)
if err != nil {
return false, err
}
headerMatcher, err := matcher.getSubMatcher()
if err != nil {
return false, err
}
return headerMatcher.Match(headerValue)
}
func (matcher *HaveHTTPHeaderWithValueMatcher) FailureMessage(actual interface{}) string {
headerValue, err := matcher.extractHeader(actual)
if err != nil {
panic(err) // protected by Match()
}
headerMatcher, err := matcher.getSubMatcher()
if err != nil {
panic(err) // protected by Match()
}
diff := format.IndentString(headerMatcher.FailureMessage(headerValue), 1)
return fmt.Sprintf("HTTP header %q:\n%s", matcher.Header, diff)
}
func (matcher *HaveHTTPHeaderWithValueMatcher) NegatedFailureMessage(actual interface{}) (message string) {
headerValue, err := matcher.extractHeader(actual)
if err != nil {
panic(err) // protected by Match()
}
headerMatcher, err := matcher.getSubMatcher()
if err != nil {
panic(err) // protected by Match()
}
diff := format.IndentString(headerMatcher.NegatedFailureMessage(headerValue), 1)
return fmt.Sprintf("HTTP header %q:\n%s", matcher.Header, diff)
}
func (matcher *HaveHTTPHeaderWithValueMatcher) getSubMatcher() (types.GomegaMatcher, error) {
switch m := matcher.Value.(type) {
case string:
return &EqualMatcher{Expected: matcher.Value}, nil
case types.GomegaMatcher:
return m, nil
default:
return nil, fmt.Errorf("HaveHTTPHeaderWithValue matcher must be passed a string or a GomegaMatcher. Got:\n%s", format.Object(matcher.Value, 1))
}
}
func (matcher *HaveHTTPHeaderWithValueMatcher) extractHeader(actual interface{}) (string, error) {
switch r := actual.(type) {
case *http.Response:
return r.Header.Get(matcher.Header), nil
case *httptest.ResponseRecorder:
return r.Result().Header.Get(matcher.Header), nil
default:
return "", fmt.Errorf("HaveHTTPHeaderWithValue matcher expects *http.Response or *httptest.ResponseRecorder. Got:\n%s", format.Object(actual, 1))
}
}

View File

@ -2,14 +2,17 @@ package matchers
import ( import (
"fmt" "fmt"
"io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect"
"strings"
"github.com/onsi/gomega/format" "github.com/onsi/gomega/format"
) )
type HaveHTTPStatusMatcher struct { type HaveHTTPStatusMatcher struct {
Expected interface{} Expected []interface{}
} }
func (matcher *HaveHTTPStatusMatcher) Match(actual interface{}) (success bool, err error) { func (matcher *HaveHTTPStatusMatcher) Match(actual interface{}) (success bool, err error) {
@ -23,20 +26,71 @@ func (matcher *HaveHTTPStatusMatcher) Match(actual interface{}) (success bool, e
return false, fmt.Errorf("HaveHTTPStatus matcher expects *http.Response or *httptest.ResponseRecorder. Got:\n%s", format.Object(actual, 1)) return false, fmt.Errorf("HaveHTTPStatus matcher expects *http.Response or *httptest.ResponseRecorder. Got:\n%s", format.Object(actual, 1))
} }
switch e := matcher.Expected.(type) { if len(matcher.Expected) == 0 {
case int: return false, fmt.Errorf("HaveHTTPStatus matcher must be passed an int or a string. Got nothing")
return resp.StatusCode == e, nil
case string:
return resp.Status == e, nil
} }
return false, fmt.Errorf("HaveHTTPStatus matcher must be passed an int or a string. Got:\n%s", format.Object(matcher.Expected, 1)) for _, expected := range matcher.Expected {
switch e := expected.(type) {
case int:
if resp.StatusCode == e {
return true, nil
}
case string:
if resp.Status == e {
return true, nil
}
default:
return false, fmt.Errorf("HaveHTTPStatus matcher must be passed int or string types. Got:\n%s", format.Object(expected, 1))
}
}
return false, nil
} }
func (matcher *HaveHTTPStatusMatcher) FailureMessage(actual interface{}) (message string) { func (matcher *HaveHTTPStatusMatcher) FailureMessage(actual interface{}) (message string) {
return format.Message(actual, "to have HTTP status", matcher.Expected) return fmt.Sprintf("Expected\n%s\n%s\n%s", formatHttpResponse(actual), "to have HTTP status", matcher.expectedString())
} }
func (matcher *HaveHTTPStatusMatcher) NegatedFailureMessage(actual interface{}) (message string) { func (matcher *HaveHTTPStatusMatcher) NegatedFailureMessage(actual interface{}) (message string) {
return format.Message(actual, "not to have HTTP status", matcher.Expected) return fmt.Sprintf("Expected\n%s\n%s\n%s", formatHttpResponse(actual), "not to have HTTP status", matcher.expectedString())
}
func (matcher *HaveHTTPStatusMatcher) expectedString() string {
var lines []string
for _, expected := range matcher.Expected {
lines = append(lines, format.Object(expected, 1))
}
return strings.Join(lines, "\n")
}
func formatHttpResponse(input interface{}) string {
var resp *http.Response
switch r := input.(type) {
case *http.Response:
resp = r
case *httptest.ResponseRecorder:
resp = r.Result()
default:
return "cannot format invalid HTTP response"
}
body := "<nil>"
if resp.Body != nil {
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
data = []byte("<error reading body>")
}
body = format.Object(string(data), 0)
}
var s strings.Builder
s.WriteString(fmt.Sprintf("%s<%s>: {\n", format.Indent, reflect.TypeOf(input)))
s.WriteString(fmt.Sprintf("%s%sStatus: %s\n", format.Indent, format.Indent, format.Object(resp.Status, 0)))
s.WriteString(fmt.Sprintf("%s%sStatusCode: %s\n", format.Indent, format.Indent, format.Object(resp.StatusCode, 0)))
s.WriteString(fmt.Sprintf("%s%sBody: %s\n", format.Indent, format.Indent, body))
s.WriteString(fmt.Sprintf("%s}", format.Indent))
return s.String()
} }

View File

@ -9,7 +9,7 @@ import (
type WithTransformMatcher struct { type WithTransformMatcher struct {
// input // input
Transform interface{} // must be a function of one parameter that returns one value Transform interface{} // must be a function of one parameter that returns one value and an optional error
Matcher types.GomegaMatcher Matcher types.GomegaMatcher
// cached value // cached value
@ -19,6 +19,9 @@ type WithTransformMatcher struct {
transformedValue interface{} transformedValue interface{}
} }
// reflect.Type for error
var errorT = reflect.TypeOf((*error)(nil)).Elem()
func NewWithTransformMatcher(transform interface{}, matcher types.GomegaMatcher) *WithTransformMatcher { func NewWithTransformMatcher(transform interface{}, matcher types.GomegaMatcher) *WithTransformMatcher {
if transform == nil { if transform == nil {
panic("transform function cannot be nil") panic("transform function cannot be nil")
@ -27,8 +30,10 @@ func NewWithTransformMatcher(transform interface{}, matcher types.GomegaMatcher)
if txType.NumIn() != 1 { if txType.NumIn() != 1 {
panic("transform function must have 1 argument") panic("transform function must have 1 argument")
} }
if txType.NumOut() != 1 { if numout := txType.NumOut(); numout != 1 {
panic("transform function must have 1 return value") if numout != 2 || !txType.Out(1).AssignableTo(errorT) {
panic("transform function must either have 1 return value, or 1 return value plus 1 error value")
}
} }
return &WithTransformMatcher{ return &WithTransformMatcher{
@ -57,6 +62,11 @@ func (m *WithTransformMatcher) Match(actual interface{}) (bool, error) {
// call the Transform function with `actual` // call the Transform function with `actual`
fn := reflect.ValueOf(m.Transform) fn := reflect.ValueOf(m.Transform)
result := fn.Call([]reflect.Value{param}) result := fn.Call([]reflect.Value{param})
if len(result) == 2 {
if !result[1].IsNil() {
return false, fmt.Errorf("Transform function failed: %s", result[1].Interface().(error).Error())
}
}
m.transformedValue = result[0].Interface() // expect exactly one value m.transformedValue = result[0].Interface() // expect exactly one value
return m.Matcher.Match(m.transformedValue) return m.Matcher.Match(m.transformedValue)

View File

@ -66,6 +66,10 @@ func MatchMayChangeInTheFuture(matcher GomegaMatcher, value interface{}) bool {
type AsyncAssertion interface { type AsyncAssertion interface {
Should(matcher GomegaMatcher, optionalDescription ...interface{}) bool Should(matcher GomegaMatcher, optionalDescription ...interface{}) bool
ShouldNot(matcher GomegaMatcher, optionalDescription ...interface{}) bool ShouldNot(matcher GomegaMatcher, optionalDescription ...interface{}) bool
WithOffset(offset int) AsyncAssertion
WithTimeout(interval time.Duration) AsyncAssertion
WithPolling(interval time.Duration) AsyncAssertion
} }
// Assertions are returned by Ω and Expect and enable assertions against Gomega matchers // Assertions are returned by Ω and Expect and enable assertions against Gomega matchers
@ -76,4 +80,8 @@ type Assertion interface {
To(matcher GomegaMatcher, optionalDescription ...interface{}) bool To(matcher GomegaMatcher, optionalDescription ...interface{}) bool
ToNot(matcher GomegaMatcher, optionalDescription ...interface{}) bool ToNot(matcher GomegaMatcher, optionalDescription ...interface{}) bool
NotTo(matcher GomegaMatcher, optionalDescription ...interface{}) bool NotTo(matcher GomegaMatcher, optionalDescription ...interface{}) bool
WithOffset(offset int) Assertion
Error() Assertion
} }

6
vendor/modules.txt vendored
View File

@ -7,7 +7,7 @@ github.com/blang/semver
# github.com/cespare/xxhash/v2 v2.1.2 # github.com/cespare/xxhash/v2 v2.1.2
## explicit; go 1.11 ## explicit; go 1.11
github.com/cespare/xxhash/v2 github.com/cespare/xxhash/v2
# github.com/containernetworking/cni v1.0.1 # github.com/containernetworking/cni v1.1.2
## explicit; go 1.14 ## explicit; go 1.14
github.com/containernetworking/cni/libcni github.com/containernetworking/cni/libcni
github.com/containernetworking/cni/pkg/invoke github.com/containernetworking/cni/pkg/invoke
@ -125,8 +125,8 @@ github.com/onsi/ginkgo/reporters/stenographer
github.com/onsi/ginkgo/reporters/stenographer/support/go-colorable github.com/onsi/ginkgo/reporters/stenographer/support/go-colorable
github.com/onsi/ginkgo/reporters/stenographer/support/go-isatty github.com/onsi/ginkgo/reporters/stenographer/support/go-isatty
github.com/onsi/ginkgo/types github.com/onsi/ginkgo/types
# github.com/onsi/gomega v1.15.0 # github.com/onsi/gomega v1.17.0
## explicit; go 1.14 ## explicit; go 1.16
github.com/onsi/gomega github.com/onsi/gomega
github.com/onsi/gomega/format github.com/onsi/gomega/format
github.com/onsi/gomega/internal github.com/onsi/gomega/internal