mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 19:31:44 +00:00
Merge pull request #29479 from timstclair/summary-test-matchers
Automatic merge from submit-queue Rewrite summary e2e test to check metric sanity Take two, forked from https://github.com/kubernetes/kubernetes/pull/28195 Adds a test library that extends the ginkgo matchers to check nested data structures. Then uses the new matcher library to thoroughly check the validity of every field in the summary metrics API. This approach is more flexible than the previous approach since it allows for different tests per-field, and is easier to add case-by-case exceptions. It also places the lower & upper bounds side-by-side, making the test much easier to read & reason about. Most fields are expected to be within some bounds. This is not intended to be a performance test, so metric bounds are very loose. Rather, I'm looking to check that the values are sane to catch bugs like #27194 Fixes #23411, https://github.com/kubernetes/kubernetes/issues/31989 /cc @kubernetes/sig-node
This commit is contained in:
commit
d5002d7e1e
58
Godeps/Godeps.json
generated
58
Godeps/Godeps.json
generated
@ -1666,63 +1666,73 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega",
|
||||
"Comment": "v1.0-115-g9ed8da1",
|
||||
"Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1"
|
||||
"Comment": "v1.0-122-gd59fa0a",
|
||||
"Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/format",
|
||||
"Comment": "v1.0-115-g9ed8da1",
|
||||
"Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1"
|
||||
"Comment": "v1.0-122-gd59fa0a",
|
||||
"Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/gstruct",
|
||||
"Comment": "v1.0-122-gd59fa0a",
|
||||
"Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/gstruct/errors",
|
||||
"Comment": "v1.0-122-gd59fa0a",
|
||||
"Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/internal/assertion",
|
||||
"Comment": "v1.0-115-g9ed8da1",
|
||||
"Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1"
|
||||
"Comment": "v1.0-122-gd59fa0a",
|
||||
"Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/internal/asyncassertion",
|
||||
"Comment": "v1.0-115-g9ed8da1",
|
||||
"Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1"
|
||||
"Comment": "v1.0-122-gd59fa0a",
|
||||
"Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/internal/oraclematcher",
|
||||
"Comment": "v1.0-115-g9ed8da1",
|
||||
"Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1"
|
||||
"Comment": "v1.0-122-gd59fa0a",
|
||||
"Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/internal/testingtsupport",
|
||||
"Comment": "v1.0-115-g9ed8da1",
|
||||
"Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1"
|
||||
"Comment": "v1.0-122-gd59fa0a",
|
||||
"Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/matchers",
|
||||
"Comment": "v1.0-115-g9ed8da1",
|
||||
"Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1"
|
||||
"Comment": "v1.0-122-gd59fa0a",
|
||||
"Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/matchers/support/goraph/bipartitegraph",
|
||||
"Comment": "v1.0-115-g9ed8da1",
|
||||
"Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1"
|
||||
"Comment": "v1.0-122-gd59fa0a",
|
||||
"Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/matchers/support/goraph/edge",
|
||||
"Comment": "v1.0-115-g9ed8da1",
|
||||
"Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1"
|
||||
"Comment": "v1.0-122-gd59fa0a",
|
||||
"Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/matchers/support/goraph/node",
|
||||
"Comment": "v1.0-115-g9ed8da1",
|
||||
"Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1"
|
||||
"Comment": "v1.0-122-gd59fa0a",
|
||||
"Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/matchers/support/goraph/util",
|
||||
"Comment": "v1.0-115-g9ed8da1",
|
||||
"Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1"
|
||||
"Comment": "v1.0-122-gd59fa0a",
|
||||
"Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/types",
|
||||
"Comment": "v1.0-115-g9ed8da1",
|
||||
"Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1"
|
||||
"Comment": "v1.0-122-gd59fa0a",
|
||||
"Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/opencontainers/runc/libcontainer",
|
||||
|
56
Godeps/LICENSES
generated
56
Godeps/LICENSES
generated
@ -53508,6 +53508,62 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
================================================================================
|
||||
|
||||
|
||||
================================================================================
|
||||
= vendor/github.com/onsi/gomega/gstruct licensed under: =
|
||||
|
||||
Copyright (c) 2013-2014 Onsi Fakhouri
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
= vendor/github.com/onsi/gomega/LICENSE 570603114d52313cb86c0206401c9af7 -
|
||||
================================================================================
|
||||
|
||||
|
||||
================================================================================
|
||||
= vendor/github.com/onsi/gomega/gstruct/errors licensed under: =
|
||||
|
||||
Copyright (c) 2013-2014 Onsi Fakhouri
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
= vendor/github.com/onsi/gomega/LICENSE 570603114d52313cb86c0206401c9af7 -
|
||||
================================================================================
|
||||
|
||||
|
||||
================================================================================
|
||||
= vendor/github.com/onsi/gomega/internal/assertion licensed under: =
|
||||
|
||||
|
@ -18,21 +18,14 @@ package e2e_node
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
apiUnversioned "k8s.io/kubernetes/pkg/api/unversioned"
|
||||
"k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/stats"
|
||||
"k8s.io/kubernetes/pkg/util/sets"
|
||||
"k8s.io/kubernetes/pkg/util/uuid"
|
||||
"k8s.io/kubernetes/test/e2e/framework"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
@ -158,322 +151,4 @@ var _ = framework.KubeDescribe("Kubelet", func() {
|
||||
}, time.Minute, time.Second*4).Should(Equal("sh: can't create /file: Read-only file system\n"))
|
||||
})
|
||||
})
|
||||
Describe("metrics api", func() {
|
||||
Context("when querying /stats/summary", func() {
|
||||
It("it should report resource usage through the stats api", func() {
|
||||
podNamePrefix := "stats-busybox-" + string(uuid.NewUUID())
|
||||
volumeNamePrefix := "test-empty-dir"
|
||||
podNames, volumes := createSummaryTestPods(f.PodClient(), podNamePrefix, 2, volumeNamePrefix)
|
||||
By("Returning stats summary")
|
||||
summary := stats.Summary{}
|
||||
Eventually(func() error {
|
||||
resp, err := http.Get(*kubeletAddress + "/stats/summary")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to get /stats/summary - %v", err)
|
||||
}
|
||||
contentsBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to read /stats/summary - %+v", resp)
|
||||
}
|
||||
contents := string(contentsBytes)
|
||||
decoder := json.NewDecoder(strings.NewReader(contents))
|
||||
err = decoder.Decode(&summary)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse /stats/summary to go struct: %+v", resp)
|
||||
}
|
||||
missingPods := podsMissingFromSummary(summary, podNames)
|
||||
if missingPods.Len() != 0 {
|
||||
return fmt.Errorf("expected pods not found. Following pods are missing - %v", missingPods)
|
||||
}
|
||||
missingVolumes := volumesMissingFromSummary(summary, volumes)
|
||||
if missingVolumes.Len() != 0 {
|
||||
return fmt.Errorf("expected volumes not found. Following volumes are missing - %v", missingVolumes)
|
||||
}
|
||||
if err := testSummaryMetrics(summary, podNamePrefix); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}, 5*time.Minute, time.Second*4).Should(BeNil())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const (
|
||||
containerSuffix = "-c"
|
||||
)
|
||||
|
||||
func createSummaryTestPods(podClient *framework.PodClient, podNamePrefix string, count int, volumeNamePrefix string) (sets.String, sets.String) {
|
||||
podNames := sets.NewString()
|
||||
volumes := sets.NewString(volumeNamePrefix)
|
||||
for i := 0; i < count; i++ {
|
||||
podNames.Insert(fmt.Sprintf("%s%v", podNamePrefix, i))
|
||||
}
|
||||
|
||||
var pods []*api.Pod
|
||||
for _, podName := range podNames.List() {
|
||||
pods = append(pods, &api.Pod{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: podName,
|
||||
},
|
||||
Spec: api.PodSpec{
|
||||
// Don't restart the Pod since it is expected to exit
|
||||
RestartPolicy: api.RestartPolicyNever,
|
||||
Containers: []api.Container{
|
||||
{
|
||||
Image: "gcr.io/google_containers/busybox:1.24",
|
||||
Command: []string{"sh", "-c", "while true; do echo 'hello world' | tee /test-empty-dir-mnt/file ; sleep 1; done"},
|
||||
Name: podName + containerSuffix,
|
||||
VolumeMounts: []api.VolumeMount{
|
||||
{MountPath: "/test-empty-dir-mnt", Name: volumeNamePrefix},
|
||||
},
|
||||
},
|
||||
},
|
||||
SecurityContext: &api.PodSecurityContext{
|
||||
SELinuxOptions: &api.SELinuxOptions{
|
||||
Level: "s0",
|
||||
},
|
||||
},
|
||||
Volumes: []api.Volume{
|
||||
// TODO: Test secret volumes
|
||||
// TODO: Test hostpath volumes
|
||||
{Name: volumeNamePrefix, VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
podClient.CreateBatch(pods)
|
||||
|
||||
return podNames, volumes
|
||||
}
|
||||
|
||||
// Returns pods missing from summary.
|
||||
func podsMissingFromSummary(s stats.Summary, expectedPods sets.String) sets.String {
|
||||
expectedPods = sets.StringKeySet(expectedPods)
|
||||
for _, pod := range s.Pods {
|
||||
if expectedPods.Has(pod.PodRef.Name) {
|
||||
expectedPods.Delete(pod.PodRef.Name)
|
||||
}
|
||||
}
|
||||
return expectedPods
|
||||
}
|
||||
|
||||
// Returns volumes missing from summary.
|
||||
func volumesMissingFromSummary(s stats.Summary, expectedVolumes sets.String) sets.String {
|
||||
for _, pod := range s.Pods {
|
||||
expectedPodVolumes := sets.StringKeySet(expectedVolumes)
|
||||
for _, vs := range pod.VolumeStats {
|
||||
if expectedPodVolumes.Has(vs.Name) {
|
||||
expectedPodVolumes.Delete(vs.Name)
|
||||
}
|
||||
}
|
||||
if expectedPodVolumes.Len() != 0 {
|
||||
return expectedPodVolumes
|
||||
}
|
||||
}
|
||||
return sets.NewString()
|
||||
}
|
||||
|
||||
func testSummaryMetrics(s stats.Summary, podNamePrefix string) error {
|
||||
const (
|
||||
nonNilValue = "expected %q to not be nil"
|
||||
nonZeroValue = "expected %q to not be zero"
|
||||
)
|
||||
if s.Node.NodeName != framework.TestContext.NodeName {
|
||||
return fmt.Errorf("unexpected node name - %q", s.Node.NodeName)
|
||||
}
|
||||
if s.Node.CPU.UsageCoreNanoSeconds == nil {
|
||||
return fmt.Errorf(nonNilValue, "cpu instantaneous")
|
||||
}
|
||||
if *s.Node.CPU.UsageCoreNanoSeconds == 0 {
|
||||
return fmt.Errorf(nonZeroValue, "cpu instantaneous")
|
||||
}
|
||||
if s.Node.Memory.UsageBytes == nil {
|
||||
return fmt.Errorf(nonNilValue, "memory")
|
||||
}
|
||||
if *s.Node.Memory.UsageBytes == 0 {
|
||||
return fmt.Errorf(nonZeroValue, "memory")
|
||||
}
|
||||
if s.Node.Memory.WorkingSetBytes == nil {
|
||||
return fmt.Errorf(nonNilValue, "memory working set")
|
||||
}
|
||||
if *s.Node.Memory.WorkingSetBytes == 0 {
|
||||
return fmt.Errorf(nonZeroValue, "memory working set")
|
||||
}
|
||||
if s.Node.Fs.AvailableBytes == nil {
|
||||
return fmt.Errorf(nonNilValue, "memory working set")
|
||||
}
|
||||
if *s.Node.Fs.AvailableBytes == 0 {
|
||||
return fmt.Errorf(nonZeroValue, "node Fs available")
|
||||
}
|
||||
if s.Node.Fs.CapacityBytes == nil {
|
||||
return fmt.Errorf(nonNilValue, "node fs capacity")
|
||||
}
|
||||
if *s.Node.Fs.CapacityBytes == 0 {
|
||||
return fmt.Errorf(nonZeroValue, "node fs capacity")
|
||||
}
|
||||
if s.Node.Fs.UsedBytes == nil {
|
||||
return fmt.Errorf(nonNilValue, "node fs used")
|
||||
}
|
||||
if *s.Node.Fs.UsedBytes == 0 {
|
||||
return fmt.Errorf(nonZeroValue, "node fs used")
|
||||
}
|
||||
|
||||
if s.Node.Runtime == nil {
|
||||
return fmt.Errorf(nonNilValue, "node runtime")
|
||||
}
|
||||
if s.Node.Runtime.ImageFs == nil {
|
||||
return fmt.Errorf(nonNilValue, "runtime image Fs")
|
||||
}
|
||||
if s.Node.Runtime.ImageFs.AvailableBytes == nil {
|
||||
return fmt.Errorf(nonNilValue, "runtime image Fs available")
|
||||
}
|
||||
if *s.Node.Runtime.ImageFs.AvailableBytes == 0 {
|
||||
return fmt.Errorf(nonZeroValue, "runtime image Fs available")
|
||||
}
|
||||
if s.Node.Runtime.ImageFs.CapacityBytes == nil {
|
||||
return fmt.Errorf(nonNilValue, "runtime image Fs capacity")
|
||||
}
|
||||
if *s.Node.Runtime.ImageFs.CapacityBytes == 0 {
|
||||
return fmt.Errorf(nonZeroValue, "runtime image Fs capacity")
|
||||
}
|
||||
if s.Node.Runtime.ImageFs.UsedBytes == nil {
|
||||
return fmt.Errorf(nonNilValue, "runtime image Fs usage")
|
||||
}
|
||||
if *s.Node.Runtime.ImageFs.UsedBytes == 0 {
|
||||
return fmt.Errorf(nonZeroValue, "runtime image Fs usage")
|
||||
}
|
||||
sysContainers := map[string]stats.ContainerStats{}
|
||||
for _, container := range s.Node.SystemContainers {
|
||||
sysContainers[container.Name] = container
|
||||
if err := expectContainerStatsNotEmpty(&container); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, exists := sysContainers["kubelet"]; !exists {
|
||||
return fmt.Errorf("expected metrics for kubelet")
|
||||
}
|
||||
if _, exists := sysContainers["runtime"]; !exists {
|
||||
return fmt.Errorf("expected metrics for runtime")
|
||||
}
|
||||
// Verify Pods Stats are present
|
||||
podsList := []string{}
|
||||
By("Having resources for pods")
|
||||
for _, pod := range s.Pods {
|
||||
if !strings.HasPrefix(pod.PodRef.Name, podNamePrefix) {
|
||||
// Ignore pods created outside this test
|
||||
continue
|
||||
}
|
||||
|
||||
podsList = append(podsList, pod.PodRef.Name)
|
||||
|
||||
if len(pod.Containers) != 1 {
|
||||
return fmt.Errorf("expected only one container")
|
||||
}
|
||||
container := pod.Containers[0]
|
||||
|
||||
if container.Name != (pod.PodRef.Name + containerSuffix) {
|
||||
return fmt.Errorf("unexpected container name - %q", container.Name)
|
||||
}
|
||||
|
||||
if err := expectContainerStatsNotEmpty(&container); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// emptydir volume
|
||||
foundExpectedVolume := false
|
||||
for _, vs := range pod.VolumeStats {
|
||||
if *vs.CapacityBytes == 0 {
|
||||
return fmt.Errorf(nonZeroValue, "volume capacity")
|
||||
}
|
||||
if *vs.AvailableBytes == 0 {
|
||||
return fmt.Errorf(nonZeroValue, "volume available")
|
||||
}
|
||||
if *vs.UsedBytes == 0 {
|
||||
return fmt.Errorf(nonZeroValue, "volume used")
|
||||
}
|
||||
if vs.Name == "test-empty-dir" {
|
||||
foundExpectedVolume = true
|
||||
}
|
||||
}
|
||||
if !foundExpectedVolume {
|
||||
return fmt.Errorf("expected 'test-empty-dir' volume")
|
||||
}
|
||||
|
||||
// fs usage (not for system containers)
|
||||
if container.Rootfs == nil {
|
||||
return fmt.Errorf(nonNilValue+" - "+spew.Sdump(container), "container root fs")
|
||||
}
|
||||
if container.Rootfs.AvailableBytes == nil {
|
||||
return fmt.Errorf(nonNilValue+" - "+spew.Sdump(container), "container root fs available")
|
||||
}
|
||||
if *container.Rootfs.AvailableBytes == 0 {
|
||||
return fmt.Errorf(nonZeroValue+" - "+spew.Sdump(container), "container root fs available")
|
||||
}
|
||||
if container.Rootfs.CapacityBytes == nil {
|
||||
return fmt.Errorf(nonNilValue+" - "+spew.Sdump(container), "container root fs capacity")
|
||||
}
|
||||
if *container.Rootfs.CapacityBytes == 0 {
|
||||
return fmt.Errorf(nonZeroValue+" - "+spew.Sdump(container), "container root fs capacity")
|
||||
}
|
||||
if container.Rootfs.UsedBytes == nil {
|
||||
return fmt.Errorf(nonNilValue+" - "+spew.Sdump(container), "container root fs usage")
|
||||
}
|
||||
if *container.Rootfs.UsedBytes == 0 {
|
||||
return fmt.Errorf(nonZeroValue+" - "+spew.Sdump(container), "container root fs usage")
|
||||
}
|
||||
if container.Logs == nil {
|
||||
return fmt.Errorf(nonNilValue+" - "+spew.Sdump(container), "container logs")
|
||||
}
|
||||
if container.Logs.AvailableBytes == nil {
|
||||
return fmt.Errorf(nonNilValue+" - "+spew.Sdump(container), "container logs available")
|
||||
}
|
||||
if *container.Logs.AvailableBytes == 0 {
|
||||
return fmt.Errorf(nonZeroValue+" - "+spew.Sdump(container), "container logs available")
|
||||
}
|
||||
if container.Logs.CapacityBytes == nil {
|
||||
return fmt.Errorf(nonNilValue+" - "+spew.Sdump(container), "container logs capacity")
|
||||
}
|
||||
if *container.Logs.CapacityBytes == 0 {
|
||||
return fmt.Errorf(nonZeroValue+" - "+spew.Sdump(container), "container logs capacity")
|
||||
}
|
||||
if container.Logs.UsedBytes == nil {
|
||||
return fmt.Errorf(nonNilValue+" - "+spew.Sdump(container), "container logs usage")
|
||||
}
|
||||
if *container.Logs.UsedBytes == 0 {
|
||||
return fmt.Errorf(nonZeroValue+" - "+spew.Sdump(container), "container logs usage")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func expectContainerStatsNotEmpty(container *stats.ContainerStats) error {
|
||||
// TODO: Test Network
|
||||
|
||||
if container.CPU == nil {
|
||||
return fmt.Errorf("expected container cpu to be not nil - %q", spew.Sdump(container))
|
||||
}
|
||||
if container.CPU.UsageCoreNanoSeconds == nil {
|
||||
return fmt.Errorf("expected container cpu instantaneous usage to be not nil - %q", spew.Sdump(container))
|
||||
}
|
||||
if *container.CPU.UsageCoreNanoSeconds == 0 {
|
||||
return fmt.Errorf("expected container cpu instantaneous usage to be non zero - %q", spew.Sdump(container))
|
||||
}
|
||||
|
||||
if container.Memory == nil {
|
||||
return fmt.Errorf("expected container memory to be not nil - %q", spew.Sdump(container))
|
||||
}
|
||||
if container.Memory.UsageBytes == nil {
|
||||
return fmt.Errorf("expected container memory usage to be not nil - %q", spew.Sdump(container))
|
||||
}
|
||||
if *container.Memory.UsageBytes == 0 {
|
||||
return fmt.Errorf("expected container memory usage to be non zero - %q", spew.Sdump(container))
|
||||
}
|
||||
if container.Memory.WorkingSetBytes == nil {
|
||||
return fmt.Errorf("expected container memory working set to be not nil - %q", spew.Sdump(container))
|
||||
}
|
||||
if *container.Memory.WorkingSetBytes == 0 {
|
||||
return fmt.Errorf("expected container memory working set to be non zero - %q", spew.Sdump(container))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
279
test/e2e_node/summary_test.go
Normal file
279
test/e2e_node/summary_test.go
Normal file
@ -0,0 +1,279 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package e2e_node
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/resource"
|
||||
"k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/stats"
|
||||
"k8s.io/kubernetes/test/e2e/framework"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gstruct"
|
||||
"github.com/onsi/gomega/types"
|
||||
)
|
||||
|
||||
// TODO(timstclair): Move this test out of the flaky suite once it has demonstrated stability.
|
||||
var _ = framework.KubeDescribe("Summary API [Flaky]", func() {
|
||||
f := framework.NewDefaultFramework("summary-test")
|
||||
Context("when querying /stats/summary", func() {
|
||||
It("should report resource usage through the stats api", func() {
|
||||
const pod0 = "stats-busybox-0"
|
||||
const pod1 = "stats-busybox-1"
|
||||
|
||||
By("Creating test pods")
|
||||
createSummaryTestPods(f, pod0, pod1)
|
||||
// Wait for cAdvisor to collect 2 stats points
|
||||
time.Sleep(15 * time.Second)
|
||||
|
||||
// Setup expectations.
|
||||
const (
|
||||
kb = 1000
|
||||
mb = 1000 * kb
|
||||
gb = 1000 * mb
|
||||
tb = 1000 * gb
|
||||
|
||||
maxStartAge = time.Hour * 24 * 365 // 1 year
|
||||
maxStatsAge = time.Minute
|
||||
)
|
||||
fsCapacityBounds := bounded(100*mb, 100*gb)
|
||||
// Expectations for system containers.
|
||||
sysContExpectations := gstruct.MatchAllFields(gstruct.Fields{
|
||||
"Name": gstruct.Ignore(),
|
||||
"StartTime": recent(maxStartAge),
|
||||
"CPU": ptrMatchAllFields(gstruct.Fields{
|
||||
"Time": recent(maxStatsAge),
|
||||
"UsageNanoCores": bounded(100000, 2E9),
|
||||
"UsageCoreNanoSeconds": bounded(10000000, 1E15),
|
||||
}),
|
||||
"Memory": ptrMatchAllFields(gstruct.Fields{
|
||||
"Time": recent(maxStatsAge),
|
||||
// We don't limit system container memory.
|
||||
"AvailableBytes": BeNil(),
|
||||
"UsageBytes": bounded(5*mb, 1*gb),
|
||||
"WorkingSetBytes": bounded(5*mb, 1*gb),
|
||||
"RSSBytes": bounded(5*mb, 1*gb),
|
||||
"PageFaults": bounded(1000, 1E9),
|
||||
"MajorPageFaults": bounded(0, 100000),
|
||||
}),
|
||||
// TODO(#31999): Don't report FS stats for system containers.
|
||||
"Rootfs": gstruct.Ignore(),
|
||||
"Logs": gstruct.Ignore(),
|
||||
"UserDefinedMetrics": BeEmpty(),
|
||||
})
|
||||
// Expectations for pods.
|
||||
podExpectations := gstruct.MatchAllFields(gstruct.Fields{
|
||||
"PodRef": gstruct.Ignore(),
|
||||
"StartTime": recent(maxStartAge),
|
||||
"Containers": gstruct.MatchAllElements(summaryObjectID, gstruct.Elements{
|
||||
"busybox-container": gstruct.MatchAllFields(gstruct.Fields{
|
||||
"Name": Equal("busybox-container"),
|
||||
"StartTime": recent(maxStartAge),
|
||||
"CPU": ptrMatchAllFields(gstruct.Fields{
|
||||
"Time": recent(maxStatsAge),
|
||||
"UsageNanoCores": bounded(100000, 100000000),
|
||||
"UsageCoreNanoSeconds": bounded(10000000, 1000000000),
|
||||
}),
|
||||
"Memory": ptrMatchAllFields(gstruct.Fields{
|
||||
"Time": recent(maxStatsAge),
|
||||
"AvailableBytes": bounded(1*mb, 10*mb),
|
||||
"UsageBytes": bounded(10*kb, mb),
|
||||
"WorkingSetBytes": bounded(10*kb, mb),
|
||||
"RSSBytes": bounded(1*kb, mb),
|
||||
"PageFaults": bounded(100, 100000),
|
||||
"MajorPageFaults": bounded(0, 10),
|
||||
}),
|
||||
"Rootfs": ptrMatchAllFields(gstruct.Fields{
|
||||
"AvailableBytes": fsCapacityBounds,
|
||||
"CapacityBytes": fsCapacityBounds,
|
||||
"UsedBytes": bounded(kb, 10*mb),
|
||||
"InodesFree": bounded(1E4, 1E8),
|
||||
"Inodes": bounded(1E4, 1E8),
|
||||
}),
|
||||
"Logs": ptrMatchAllFields(gstruct.Fields{
|
||||
"AvailableBytes": fsCapacityBounds,
|
||||
"CapacityBytes": fsCapacityBounds,
|
||||
"UsedBytes": bounded(kb, 10*mb),
|
||||
"InodesFree": bounded(1E4, 1E8),
|
||||
"Inodes": bounded(1E4, 1E8),
|
||||
}),
|
||||
"UserDefinedMetrics": BeEmpty(),
|
||||
}),
|
||||
}),
|
||||
"Network": ptrMatchAllFields(gstruct.Fields{
|
||||
"Time": recent(maxStatsAge),
|
||||
"RxBytes": bounded(10, 10*mb),
|
||||
"RxErrors": bounded(0, 1000),
|
||||
"TxBytes": bounded(10, 10*mb),
|
||||
"TxErrors": bounded(0, 1000),
|
||||
}),
|
||||
"VolumeStats": gstruct.MatchAllElements(summaryObjectID, gstruct.Elements{
|
||||
"test-empty-dir": gstruct.MatchAllFields(gstruct.Fields{
|
||||
"Name": Equal("test-empty-dir"),
|
||||
"FsStats": gstruct.MatchAllFields(gstruct.Fields{
|
||||
"AvailableBytes": fsCapacityBounds,
|
||||
"CapacityBytes": fsCapacityBounds,
|
||||
"UsedBytes": bounded(kb, 1*mb),
|
||||
// Inodes are not reported for Volumes.
|
||||
"InodesFree": BeNil(),
|
||||
"Inodes": BeNil(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
matchExpectations := ptrMatchAllFields(gstruct.Fields{
|
||||
"Node": gstruct.MatchAllFields(gstruct.Fields{
|
||||
"NodeName": Equal(framework.TestContext.NodeName),
|
||||
"StartTime": recent(maxStartAge),
|
||||
"SystemContainers": gstruct.MatchElements(summaryObjectID, gstruct.IgnoreExtras, gstruct.Elements{
|
||||
"kubelet": sysContExpectations,
|
||||
"runtime": sysContExpectations,
|
||||
}),
|
||||
"CPU": ptrMatchAllFields(gstruct.Fields{
|
||||
"Time": recent(maxStatsAge),
|
||||
"UsageNanoCores": bounded(100E3, 2E9),
|
||||
"UsageCoreNanoSeconds": bounded(1E9, 1E15),
|
||||
}),
|
||||
"Memory": ptrMatchAllFields(gstruct.Fields{
|
||||
"Time": recent(maxStatsAge),
|
||||
"AvailableBytes": bounded(100*mb, 100*gb),
|
||||
"UsageBytes": bounded(10*mb, 10*gb),
|
||||
"WorkingSetBytes": bounded(10*mb, 10*gb),
|
||||
"RSSBytes": bounded(1*kb, 1*gb),
|
||||
"PageFaults": bounded(1000, 1E9),
|
||||
"MajorPageFaults": bounded(0, 100000),
|
||||
}),
|
||||
// TODO(#28407): Handle non-eth0 network interface names.
|
||||
"Network": Or(BeNil(), ptrMatchAllFields(gstruct.Fields{
|
||||
"Time": recent(maxStatsAge),
|
||||
"RxBytes": bounded(1*mb, 100*gb),
|
||||
"RxErrors": bounded(0, 100000),
|
||||
"TxBytes": bounded(10*kb, 10*gb),
|
||||
"TxErrors": bounded(0, 100000),
|
||||
})),
|
||||
"Fs": ptrMatchAllFields(gstruct.Fields{
|
||||
"AvailableBytes": fsCapacityBounds,
|
||||
"CapacityBytes": fsCapacityBounds,
|
||||
"UsedBytes": bounded(kb, 10*gb),
|
||||
"InodesFree": bounded(1E4, 1E8),
|
||||
"Inodes": bounded(1E4, 1E8),
|
||||
}),
|
||||
"Runtime": ptrMatchAllFields(gstruct.Fields{
|
||||
"ImageFs": ptrMatchAllFields(gstruct.Fields{
|
||||
"AvailableBytes": fsCapacityBounds,
|
||||
"CapacityBytes": fsCapacityBounds,
|
||||
"UsedBytes": bounded(kb, 10*gb),
|
||||
"InodesFree": bounded(1E4, 1E8),
|
||||
"Inodes": bounded(1E4, 1E8),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
// Ignore extra pods since the tests run in parallel.
|
||||
"Pods": gstruct.MatchElements(summaryObjectID, gstruct.IgnoreExtras, gstruct.Elements{
|
||||
fmt.Sprintf("%s::%s", f.Namespace.Name, pod0): podExpectations,
|
||||
fmt.Sprintf("%s::%s", f.Namespace.Name, pod1): podExpectations,
|
||||
}),
|
||||
})
|
||||
|
||||
By("Validating /stats/summary")
|
||||
// Give pods a minute to actually start up.
|
||||
Eventually(getNodeSummary, 1*time.Minute, 15*time.Second).Should(matchExpectations)
|
||||
// Then the summary should match the expectations a few more times.
|
||||
Consistently(getNodeSummary, 30*time.Second, 15*time.Second).Should(matchExpectations)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
func createSummaryTestPods(f *framework.Framework, names ...string) {
|
||||
pods := make([]*api.Pod, 0, len(names))
|
||||
for _, name := range names {
|
||||
pods = append(pods, &api.Pod{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Spec: api.PodSpec{
|
||||
RestartPolicy: api.RestartPolicyAlways,
|
||||
Containers: []api.Container{
|
||||
{
|
||||
Name: "busybox-container",
|
||||
Image: "gcr.io/google_containers/busybox:1.24",
|
||||
Command: []string{"sh", "-c", "ping -c 1 google.com; while true; do echo 'hello world' | tee /test-empty-dir-mnt/file ; sleep 1; done"},
|
||||
Resources: api.ResourceRequirements{
|
||||
Limits: api.ResourceList{
|
||||
// Must set memory limit to get MemoryStats.AvailableBytes
|
||||
api.ResourceMemory: resource.MustParse("10M"),
|
||||
},
|
||||
},
|
||||
VolumeMounts: []api.VolumeMount{
|
||||
{MountPath: "/test-empty-dir-mnt", Name: "test-empty-dir"},
|
||||
},
|
||||
},
|
||||
},
|
||||
SecurityContext: &api.PodSecurityContext{
|
||||
SELinuxOptions: &api.SELinuxOptions{
|
||||
Level: "s0",
|
||||
},
|
||||
},
|
||||
Volumes: []api.Volume{
|
||||
// TODO(#28393): Test secret volumes
|
||||
// TODO(#28394): Test hostpath volumes
|
||||
{Name: "test-empty-dir", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
f.PodClient().CreateBatch(pods)
|
||||
}
|
||||
|
||||
// Mapping function for gstruct.MatchAllElements
|
||||
func summaryObjectID(element interface{}) string {
|
||||
switch el := element.(type) {
|
||||
case stats.PodStats:
|
||||
return fmt.Sprintf("%s::%s", el.PodRef.Namespace, el.PodRef.Name)
|
||||
case stats.ContainerStats:
|
||||
return el.Name
|
||||
case stats.VolumeStats:
|
||||
return el.Name
|
||||
case stats.UserDefinedMetric:
|
||||
return el.Name
|
||||
default:
|
||||
framework.Failf("Unknown type: %T", el)
|
||||
return "???"
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience functions for common matcher combinations.
|
||||
func ptrMatchAllFields(fields gstruct.Fields) types.GomegaMatcher {
|
||||
return gstruct.PointTo(gstruct.MatchAllFields(fields))
|
||||
}
|
||||
|
||||
func bounded(lower, upper interface{}) types.GomegaMatcher {
|
||||
return gstruct.PointTo(And(
|
||||
BeNumerically(">=", lower),
|
||||
BeNumerically("<=", upper)))
|
||||
}
|
||||
|
||||
func recent(d time.Duration) types.GomegaMatcher {
|
||||
return And(
|
||||
BeTemporally(">=", time.Now().Add(-d)),
|
||||
// Now() is the test start time, not the match time, so permit a few extra minutes.
|
||||
BeTemporally("<", time.Now().Add(2*time.Minute)))
|
||||
}
|
4
vendor/github.com/onsi/gomega/README.md
generated
vendored
4
vendor/github.com/onsi/gomega/README.md
generated
vendored
@ -10,6 +10,10 @@ To discuss Gomega and get updates, join the [google group](https://groups.google
|
||||
|
||||
Learn more about Ginkgo [here](http://onsi.github.io/ginkgo/)
|
||||
|
||||
## Community Matchers
|
||||
|
||||
A collection of community matchers is available on the [wiki](https://github.com/onsi/gomega/wiki).
|
||||
|
||||
## License
|
||||
|
||||
Gomega is MIT-Licensed
|
||||
|
20
vendor/github.com/onsi/gomega/format/format.go
generated
vendored
20
vendor/github.com/onsi/gomega/format/format.go
generated
vendored
@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Use MaxDepth to set the maximum recursion depth when printing deeply nested objects
|
||||
@ -143,9 +144,6 @@ func formatValue(value reflect.Value, indentation uint) string {
|
||||
case reflect.Ptr:
|
||||
return formatValue(value.Elem(), indentation)
|
||||
case reflect.Slice:
|
||||
if value.Type().Elem().Kind() == reflect.Uint8 {
|
||||
return formatString(value.Bytes(), indentation)
|
||||
}
|
||||
return formatSlice(value, indentation)
|
||||
case reflect.String:
|
||||
return formatString(value.String(), indentation)
|
||||
@ -189,6 +187,10 @@ func formatString(object interface{}, indentation uint) string {
|
||||
}
|
||||
|
||||
func formatSlice(v reflect.Value, indentation uint) string {
|
||||
if v.Kind() == reflect.Slice && v.Type().Elem().Kind() == reflect.Uint8 && isPrintableString(string(v.Bytes())){
|
||||
return formatString(v.Bytes(), indentation)
|
||||
}
|
||||
|
||||
l := v.Len()
|
||||
result := make([]string, l)
|
||||
longest := 0
|
||||
@ -261,3 +263,15 @@ func isNilValue(a reflect.Value) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
Returns true when the string is entirely made of printable runes, false otherwise.
|
||||
*/
|
||||
func isPrintableString(str string) bool {
|
||||
for _, runeValue := range str {
|
||||
if !strconv.IsPrint(runeValue) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
141
vendor/github.com/onsi/gomega/gstruct/elements.go
generated
vendored
Normal file
141
vendor/github.com/onsi/gomega/gstruct/elements.go
generated
vendored
Normal file
@ -0,0 +1,141 @@
|
||||
package gstruct
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/onsi/gomega/format"
|
||||
errorsutil "github.com/onsi/gomega/gstruct/errors"
|
||||
"github.com/onsi/gomega/types"
|
||||
)
|
||||
|
||||
//MatchAllElements succeeds if every element of a slice matches the element matcher it maps to
|
||||
//through the id function, and every element matcher is matched.
|
||||
// Expect([]string{"a", "b"}).To(MatchAllElements(idFn, matchers.Elements{
|
||||
// "a": BeEqual("a"),
|
||||
// "b": BeEqual("b"),
|
||||
// })
|
||||
func MatchAllElements(identifier Identifier, elements Elements) types.GomegaMatcher {
|
||||
return &ElementsMatcher{
|
||||
Identifier: identifier,
|
||||
Elements: elements,
|
||||
}
|
||||
}
|
||||
|
||||
//MatchElements succeeds if each element of a slice matches the element matcher it maps to
|
||||
//through the id function. It can ignore extra elements and/or missing elements.
|
||||
// Expect([]string{"a", "c"}).To(MatchElements(idFn, IgnoreMissing|IgnoreExtra, matchers.Elements{
|
||||
// "a": BeEqual("a")
|
||||
// "b": BeEqual("b"),
|
||||
// })
|
||||
func MatchElements(identifier Identifier, options Options, elements Elements) types.GomegaMatcher {
|
||||
return &ElementsMatcher{
|
||||
Identifier: identifier,
|
||||
Elements: elements,
|
||||
IgnoreExtras: options&IgnoreExtras != 0,
|
||||
IgnoreMissing: options&IgnoreMissing != 0,
|
||||
}
|
||||
}
|
||||
|
||||
// ElementsMatcher is a NestingMatcher that applies custom matchers to each element of a slice mapped
|
||||
// by the Identifier function.
|
||||
// TODO: Extend this to work with arrays & maps (map the key) as well.
|
||||
type ElementsMatcher struct {
|
||||
// Matchers for each element.
|
||||
Elements Elements
|
||||
// Function mapping an element to the string key identifying its matcher.
|
||||
Identifier Identifier
|
||||
|
||||
// Whether to ignore extra elements or consider it an error.
|
||||
IgnoreExtras bool
|
||||
// Whether to ignore missing elements or consider it an error.
|
||||
IgnoreMissing bool
|
||||
|
||||
// State.
|
||||
failures []error
|
||||
}
|
||||
|
||||
// Element ID to matcher.
|
||||
type Elements map[string]types.GomegaMatcher
|
||||
|
||||
// Function for identifying (mapping) elements.
|
||||
type Identifier func(element interface{}) string
|
||||
|
||||
func (m *ElementsMatcher) Match(actual interface{}) (success bool, err error) {
|
||||
if reflect.TypeOf(actual).Kind() != reflect.Slice {
|
||||
return false, fmt.Errorf("%v is type %T, expected slice", actual, actual)
|
||||
}
|
||||
|
||||
m.failures = m.matchElements(actual)
|
||||
if len(m.failures) > 0 {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (m *ElementsMatcher) matchElements(actual interface{}) (errs []error) {
|
||||
// Provide more useful error messages in the case of a panic.
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("panic checking %+v: %v\n%s", actual, err, debug.Stack()))
|
||||
}
|
||||
}()
|
||||
|
||||
val := reflect.ValueOf(actual)
|
||||
elements := map[string]bool{}
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
element := val.Index(i).Interface()
|
||||
id := m.Identifier(element)
|
||||
// TODO: Add options to ignore & match duplicates.
|
||||
if elements[id] {
|
||||
errs = append(errs, fmt.Errorf("found duplicate element ID %s", id))
|
||||
continue
|
||||
}
|
||||
elements[id] = true
|
||||
|
||||
matcher, expected := m.Elements[id]
|
||||
if !expected {
|
||||
if !m.IgnoreExtras {
|
||||
errs = append(errs, fmt.Errorf("unexpected element %s", id))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
match, err := matcher.Match(element)
|
||||
if match {
|
||||
continue
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if nesting, ok := matcher.(errorsutil.NestingMatcher); ok {
|
||||
err = errorsutil.AggregateError(nesting.Failures())
|
||||
} else {
|
||||
err = errors.New(matcher.FailureMessage(element))
|
||||
}
|
||||
}
|
||||
errs = append(errs, errorsutil.Nest(fmt.Sprintf("[%s]", id), err))
|
||||
}
|
||||
|
||||
for id := range m.Elements {
|
||||
if !elements[id] && !m.IgnoreMissing {
|
||||
errs = append(errs, fmt.Errorf("missing expected element %s", id))
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (m *ElementsMatcher) FailureMessage(actual interface{}) (message string) {
|
||||
failure := errorsutil.AggregateError(m.failures)
|
||||
return format.Message(actual, fmt.Sprintf("to match elements: %v", failure))
|
||||
}
|
||||
|
||||
func (m *ElementsMatcher) NegatedFailureMessage(actual interface{}) (message string) {
|
||||
return format.Message(actual, "not to match elements")
|
||||
}
|
||||
|
||||
func (m *ElementsMatcher) Failures() []error {
|
||||
return m.failures
|
||||
}
|
72
vendor/github.com/onsi/gomega/gstruct/errors/nested_types.go
generated
vendored
Normal file
72
vendor/github.com/onsi/gomega/gstruct/errors/nested_types.go
generated
vendored
Normal file
@ -0,0 +1,72 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/onsi/gomega/types"
|
||||
)
|
||||
|
||||
// A stateful matcher that nests other matchers within it and preserves the error types of the
|
||||
// nested matcher failures.
|
||||
type NestingMatcher interface {
|
||||
types.GomegaMatcher
|
||||
|
||||
// Returns the failures of nested matchers.
|
||||
Failures() []error
|
||||
}
|
||||
|
||||
// An error type for labeling errors on deeply nested matchers.
|
||||
type NestedError struct {
|
||||
Path string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *NestedError) Error() string {
|
||||
// Indent Errors.
|
||||
indented := strings.Replace(e.Err.Error(), "\n", "\n\t", -1)
|
||||
return fmt.Sprintf("%s:\n\t%v", e.Path, indented)
|
||||
}
|
||||
|
||||
// Create a NestedError with the given path.
|
||||
// If err is a NestedError, prepend the path to it.
|
||||
// If err is an AggregateError, recursively Nest each error.
|
||||
func Nest(path string, err error) error {
|
||||
if ag, ok := err.(AggregateError); ok {
|
||||
var errs AggregateError
|
||||
for _, e := range ag {
|
||||
errs = append(errs, Nest(path, e))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
if ne, ok := err.(*NestedError); ok {
|
||||
return &NestedError{
|
||||
Path: path + ne.Path,
|
||||
Err: ne.Err,
|
||||
}
|
||||
}
|
||||
return &NestedError{
|
||||
Path: path,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// An error type for treating multiple errors as a single error.
|
||||
type AggregateError []error
|
||||
|
||||
// Error is part of the error interface.
|
||||
func (err AggregateError) Error() string {
|
||||
if len(err) == 0 {
|
||||
// This should never happen, really.
|
||||
return ""
|
||||
}
|
||||
if len(err) == 1 {
|
||||
return err[0].Error()
|
||||
}
|
||||
result := fmt.Sprintf("[%s", err[0].Error())
|
||||
for i := 1; i < len(err); i++ {
|
||||
result += fmt.Sprintf(", %s", err[i].Error())
|
||||
}
|
||||
result += "]"
|
||||
return result
|
||||
}
|
141
vendor/github.com/onsi/gomega/gstruct/fields.go
generated
vendored
Normal file
141
vendor/github.com/onsi/gomega/gstruct/fields.go
generated
vendored
Normal file
@ -0,0 +1,141 @@
|
||||
package gstruct
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/onsi/gomega/format"
|
||||
errorsutil "github.com/onsi/gomega/gstruct/errors"
|
||||
"github.com/onsi/gomega/types"
|
||||
)
|
||||
|
||||
//MatchAllFields succeeds if every field of a struct matches the field matcher associated with
|
||||
//it, and every element matcher is matched.
|
||||
// Expect([]string{"a", "b"}).To(MatchAllFields(idFn, gstruct.Fields{
|
||||
// "a": BeEqual("a"),
|
||||
// "b": BeEqual("b"),
|
||||
// })
|
||||
func MatchAllFields(fields Fields) types.GomegaMatcher {
|
||||
return &FieldsMatcher{
|
||||
Fields: fields,
|
||||
}
|
||||
}
|
||||
|
||||
//MatchFields succeeds if each element of a struct matches the field matcher associated with
|
||||
//it. It can ignore extra fields and/or missing fields.
|
||||
// Expect([]string{"a", "c"}).To(MatchFields(idFn, IgnoreMissing|IgnoreExtra, gstruct.Fields{
|
||||
// "a": BeEqual("a")
|
||||
// "b": BeEqual("b"),
|
||||
// })
|
||||
func MatchFields(options Options, fields Fields) types.GomegaMatcher {
|
||||
return &FieldsMatcher{
|
||||
Fields: fields,
|
||||
IgnoreExtras: options&IgnoreExtras != 0,
|
||||
IgnoreMissing: options&IgnoreMissing != 0,
|
||||
}
|
||||
}
|
||||
|
||||
type FieldsMatcher struct {
|
||||
// Matchers for each field.
|
||||
Fields Fields
|
||||
|
||||
// Whether to ignore extra elements or consider it an error.
|
||||
IgnoreExtras bool
|
||||
// Whether to ignore missing elements or consider it an error.
|
||||
IgnoreMissing bool
|
||||
|
||||
// State.
|
||||
failures []error
|
||||
}
|
||||
|
||||
// Field name to matcher.
|
||||
type Fields map[string]types.GomegaMatcher
|
||||
|
||||
func (m *FieldsMatcher) Match(actual interface{}) (success bool, err error) {
|
||||
if reflect.TypeOf(actual).Kind() != reflect.Struct {
|
||||
return false, fmt.Errorf("%v is type %T, expected struct", actual, actual)
|
||||
}
|
||||
|
||||
m.failures = m.matchFields(actual)
|
||||
if len(m.failures) > 0 {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (m *FieldsMatcher) matchFields(actual interface{}) (errs []error) {
|
||||
val := reflect.ValueOf(actual)
|
||||
typ := val.Type()
|
||||
fields := map[string]bool{}
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
fieldName := typ.Field(i).Name
|
||||
fields[fieldName] = true
|
||||
|
||||
err := func() (err error) {
|
||||
// This test relies heavily on reflect, which tends to panic.
|
||||
// Recover here to provide more useful error messages in that case.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic checking %+v: %v\n%s", actual, r, debug.Stack())
|
||||
}
|
||||
}()
|
||||
|
||||
matcher, expected := m.Fields[fieldName]
|
||||
if !expected {
|
||||
if !m.IgnoreExtras {
|
||||
return fmt.Errorf("unexpected field %s: %+v", fieldName, actual)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var field interface{}
|
||||
if val.Field(i).IsValid() {
|
||||
field = val.Field(i).Interface()
|
||||
} else {
|
||||
field = reflect.Zero(typ.Field(i).Type)
|
||||
}
|
||||
|
||||
match, err := matcher.Match(field)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !match {
|
||||
if nesting, ok := matcher.(errorsutil.NestingMatcher); ok {
|
||||
return errorsutil.AggregateError(nesting.Failures())
|
||||
}
|
||||
return errors.New(matcher.FailureMessage(field))
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
errs = append(errs, errorsutil.Nest("."+fieldName, err))
|
||||
}
|
||||
}
|
||||
|
||||
for field := range m.Fields {
|
||||
if !fields[field] && !m.IgnoreMissing {
|
||||
errs = append(errs, fmt.Errorf("missing expected field %s", field))
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (m *FieldsMatcher) FailureMessage(actual interface{}) (message string) {
|
||||
failures := make([]string, len(m.failures))
|
||||
for i := range m.failures {
|
||||
failures[i] = m.failures[i].Error()
|
||||
}
|
||||
return format.Message(reflect.TypeOf(actual).Name(),
|
||||
fmt.Sprintf("to match fields: {\n%v\n}\n", strings.Join(failures, "\n")))
|
||||
}
|
||||
|
||||
func (m *FieldsMatcher) NegatedFailureMessage(actual interface{}) (message string) {
|
||||
return format.Message(actual, "not to match fields")
|
||||
}
|
||||
|
||||
func (m *FieldsMatcher) Failures() []error {
|
||||
return m.failures
|
||||
}
|
37
vendor/github.com/onsi/gomega/gstruct/ignore.go
generated
vendored
Normal file
37
vendor/github.com/onsi/gomega/gstruct/ignore.go
generated
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
package gstruct
|
||||
|
||||
import (
|
||||
"github.com/onsi/gomega/types"
|
||||
)
|
||||
|
||||
//Ignore ignores the actual value and always succeeds.
|
||||
// Expect(nil).To(Ignore())
|
||||
// Expect(true).To(Ignore())
|
||||
func Ignore() types.GomegaMatcher {
|
||||
return &IgnoreMatcher{true}
|
||||
}
|
||||
|
||||
//Reject ignores the actual value and always fails. It can be used in conjunction with IgnoreMissing
|
||||
//to catch problematic elements, or to verify tests are running.
|
||||
// Expect(nil).NotTo(Reject())
|
||||
// Expect(true).NotTo(Reject())
|
||||
func Reject() types.GomegaMatcher {
|
||||
return &IgnoreMatcher{false}
|
||||
}
|
||||
|
||||
// A matcher that either always succeeds or always fails.
|
||||
type IgnoreMatcher struct {
|
||||
Succeed bool
|
||||
}
|
||||
|
||||
func (m *IgnoreMatcher) Match(actual interface{}) (bool, error) {
|
||||
return m.Succeed, nil
|
||||
}
|
||||
|
||||
func (m *IgnoreMatcher) FailureMessage(_ interface{}) (message string) {
|
||||
return "Unconditional failure"
|
||||
}
|
||||
|
||||
func (m *IgnoreMatcher) NegatedFailureMessage(_ interface{}) (message string) {
|
||||
return "Unconditional success"
|
||||
}
|
56
vendor/github.com/onsi/gomega/gstruct/pointer.go
generated
vendored
Normal file
56
vendor/github.com/onsi/gomega/gstruct/pointer.go
generated
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
package gstruct
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/onsi/gomega/format"
|
||||
"github.com/onsi/gomega/types"
|
||||
)
|
||||
|
||||
//PointTo applies the given matcher to the value pointed to by actual. It fails if the pointer is
|
||||
//nil.
|
||||
// actual := 5
|
||||
// Expect(&actual).To(PointTo(Equal(5)))
|
||||
func PointTo(matcher types.GomegaMatcher) types.GomegaMatcher {
|
||||
return &PointerMatcher{
|
||||
Matcher: matcher,
|
||||
}
|
||||
}
|
||||
|
||||
type PointerMatcher struct {
|
||||
Matcher types.GomegaMatcher
|
||||
|
||||
// Failure message.
|
||||
failure string
|
||||
}
|
||||
|
||||
func (m *PointerMatcher) Match(actual interface{}) (bool, error) {
|
||||
val := reflect.ValueOf(actual)
|
||||
|
||||
// return error if actual type is not a pointer
|
||||
if val.Kind() != reflect.Ptr {
|
||||
return false, fmt.Errorf("PointerMatcher expects a pointer but we have '%s'", val.Kind())
|
||||
}
|
||||
|
||||
if !val.IsValid() || val.IsNil() {
|
||||
m.failure = format.Message(actual, "not to be <nil>")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Forward the value.
|
||||
elem := val.Elem().Interface()
|
||||
match, err := m.Matcher.Match(elem)
|
||||
if !match {
|
||||
m.failure = m.Matcher.FailureMessage(elem)
|
||||
}
|
||||
return match, err
|
||||
}
|
||||
|
||||
func (m *PointerMatcher) FailureMessage(_ interface{}) (message string) {
|
||||
return m.failure
|
||||
}
|
||||
|
||||
func (m *PointerMatcher) NegatedFailureMessage(actual interface{}) (message string) {
|
||||
return m.Matcher.NegatedFailureMessage(actual)
|
||||
}
|
11
vendor/github.com/onsi/gomega/gstruct/types.go
generated
vendored
Normal file
11
vendor/github.com/onsi/gomega/gstruct/types.go
generated
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
package gstruct
|
||||
|
||||
//Options is the type for options passed to some matchers.
|
||||
type Options int
|
||||
|
||||
const (
|
||||
//IgnoreExtras tells the matcher to ignore extra elements or fields, rather than triggering a failure.
|
||||
IgnoreExtras Options = 1 << iota
|
||||
//IgnoreMissing tells the matcher to ignore missing elements or fields, rather than triggering a failure.
|
||||
IgnoreMissing
|
||||
)
|
Loading…
Reference in New Issue
Block a user