Importable End-To-End Test Suite (#758)
* Rename VK to chewong for development purpose * Rename basic_test.go to basic.go * Add e2e.go and suite.go * Disable tests in node.go * End to end tests are now importable as a testing suite * Remove 'test' from test files * Add documentations * Rename chewong back to virtual-kubelet * Change 'Testing Suite' to 'Test Suite' * Add the ability to skip certain testss * Add unit tests for suite.go * Add README.md for importable e2e test suite * VK implementation has to be based on VK v1.0.0 * Stricter checks on validating test functions * Move certain files back to internal folder * Add WatchTimeout as a config field * Add slight modifications
This commit is contained in:
@@ -1,364 +0,0 @@
|
||||
// +build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/virtual-kubelet/virtual-kubelet/node"
|
||||
"gotest.tools/assert"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/kubernetes/pkg/kubelet/apis/stats/v1alpha1"
|
||||
)
|
||||
|
||||
const (
|
||||
// deleteGracePeriodForProvider is the maximum amount of time we allow for the provider to react to deletion of a pod
|
||||
// before proceeding to assert that the pod has been deleted.
|
||||
deleteGracePeriodForProvider = 1 * time.Second
|
||||
)
|
||||
|
||||
// TestGetStatsSummary creates a pod having two containers and queries the /stats/summary endpoint of the virtual-kubelet.
|
||||
// It expects this endpoint to return stats for the current node, as well as for the aforementioned pod and each of its two containers.
|
||||
func TestGetStatsSummary(t *testing.T) {
|
||||
// Create a pod with prefix "nginx-" having three containers.
|
||||
pod, err := f.CreatePod(f.CreateDummyPodObjectWithPrefix(t.Name(), "nginx-", "foo", "bar", "baz"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Delete the "nginx-0-X" pod after the test finishes.
|
||||
defer func() {
|
||||
if err := f.DeletePodImmediately(pod.Namespace, pod.Name); err != nil && !apierrors.IsNotFound(err) {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for the "nginx-" pod to be reported as running and ready.
|
||||
if _, err := f.WaitUntilPodReady(pod.Namespace, pod.Name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Grab the stats from the provider.
|
||||
stats, err := f.GetStatsSummary()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Make sure that we've got stats for the current node.
|
||||
if stats.Node.NodeName != f.NodeName {
|
||||
t.Fatalf("expected stats for node %s, got stats for node %s", f.NodeName, stats.Node.NodeName)
|
||||
}
|
||||
|
||||
// Make sure the "nginx-" pod exists in the slice of PodStats.
|
||||
idx, err := findPodInPodStats(stats, pod)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Make sure that we've got stats for all the containers in the "nginx-" pod.
|
||||
desiredContainerStatsCount := len(pod.Spec.Containers)
|
||||
currentContainerStatsCount := len(stats.Pods[idx].Containers)
|
||||
if currentContainerStatsCount != desiredContainerStatsCount {
|
||||
t.Fatalf("expected stats for %d containers, got stats for %d containers", desiredContainerStatsCount, currentContainerStatsCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPodLifecycleGracefulDelete creates a pod and verifies that the provider has been asked to create it.
|
||||
// Then, it deletes the pods and verifies that the provider has been asked to delete it.
|
||||
// These verifications are made using the /stats/summary endpoint of the virtual-kubelet, by checking for the presence or absence of the pods.
|
||||
// Hence, the provider being tested must implement the PodMetricsProvider interface.
|
||||
func TestPodLifecycleGracefulDelete(t *testing.T) {
|
||||
// Create a pod with prefix "nginx-" having a single container.
|
||||
podSpec := f.CreateDummyPodObjectWithPrefix(t.Name(), "nginx-", "foo")
|
||||
podSpec.Spec.NodeName = nodeName
|
||||
|
||||
pod, err := f.CreatePod(podSpec)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Delete the pod after the test finishes.
|
||||
defer func() {
|
||||
if err := f.DeletePodImmediately(pod.Namespace, pod.Name); err != nil && !apierrors.IsNotFound(err) {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
t.Logf("Created pod: %s", pod.Name)
|
||||
|
||||
// Wait for the "nginx-" pod to be reported as running and ready.
|
||||
if _, err := f.WaitUntilPodReady(pod.Namespace, pod.Name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Pod %s ready", pod.Name)
|
||||
|
||||
// Grab the pods from the provider.
|
||||
pods, err := f.GetRunningPods()
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Check if the pod exists in the slice of PodStats.
|
||||
assert.NilError(t, findPodInPods(pods, pod))
|
||||
|
||||
podCh := make(chan error)
|
||||
var podLast *v1.Pod
|
||||
go func() {
|
||||
// Close the podCh channel, signaling we've observed deletion of the pod.
|
||||
defer close(podCh)
|
||||
|
||||
var err error
|
||||
podLast, err = f.WaitUntilPodDeleted(pod.Namespace, pod.Name)
|
||||
if err != nil {
|
||||
// Propagate the error to the outside so we can fail the test.
|
||||
podCh <- err
|
||||
}
|
||||
}()
|
||||
|
||||
// Gracefully delete the "nginx-" pod.
|
||||
if err := f.DeletePod(pod.Namespace, pod.Name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Deleted pod: %s", pod.Name)
|
||||
|
||||
// Wait for the delete event to be ACKed.
|
||||
if err := <-podCh; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
time.Sleep(deleteGracePeriodForProvider)
|
||||
// Give the provider some time to react to the MODIFIED/DELETED events before proceeding.
|
||||
// Grab the pods from the provider.
|
||||
pods, err = f.GetRunningPods()
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Make sure the pod DOES NOT exist in the provider's set of running pods
|
||||
assert.Assert(t, findPodInPods(pods, pod) != nil)
|
||||
|
||||
// Make sure we saw the delete event, and the delete event was graceful
|
||||
assert.Assert(t, podLast != nil)
|
||||
assert.Assert(t, podLast.ObjectMeta.GetDeletionGracePeriodSeconds() != nil)
|
||||
assert.Assert(t, *podLast.ObjectMeta.GetDeletionGracePeriodSeconds() > 0)
|
||||
}
|
||||
|
||||
// TestPodLifecycleNonGracefulDelete creates one podsand verifies that the provider has created them
|
||||
// and put them in the running lifecycle. It then does a force delete on the pod, and verifies the provider
|
||||
// has deleted it.
|
||||
func TestPodLifecycleForceDelete(t *testing.T) {
|
||||
podSpec := f.CreateDummyPodObjectWithPrefix(t.Name(), "nginx-", "foo")
|
||||
// Create a pod with prefix having a single container.
|
||||
pod, err := f.CreatePod(podSpec)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Delete the pod after the test finishes.
|
||||
defer func() {
|
||||
if err := f.DeletePodImmediately(pod.Namespace, pod.Name); err != nil && !apierrors.IsNotFound(err) {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
t.Logf("Created pod: %s", pod.Name)
|
||||
|
||||
// Wait for the "nginx-" pod to be reported as running and ready.
|
||||
if _, err := f.WaitUntilPodReady(pod.Namespace, pod.Name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Pod %s ready", pod.Name)
|
||||
|
||||
// Grab the pods from the provider.
|
||||
pods, err := f.GetRunningPods()
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Check if the pod exists in the slice of Pods.
|
||||
assert.NilError(t, findPodInPods(pods, pod))
|
||||
|
||||
// Wait for the pod to be deleted in a separate goroutine.
|
||||
// This ensures that we don't possibly miss the MODIFIED/DELETED events due to establishing the watch too late in the process.
|
||||
// It also makes sure that in light of soft deletes, we properly handle non-graceful pod deletion
|
||||
podCh := make(chan error)
|
||||
var podLast *v1.Pod
|
||||
go func() {
|
||||
// Close the podCh channel, signaling we've observed deletion of the pod.
|
||||
defer close(podCh)
|
||||
|
||||
var err error
|
||||
// Wait for the pod to be reported as having been deleted.
|
||||
podLast, err = f.WaitUntilPodDeleted(pod.Namespace, pod.Name)
|
||||
if err != nil {
|
||||
// Propagate the error to the outside so we can fail the test.
|
||||
podCh <- err
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(deleteGracePeriodForProvider)
|
||||
// Forcibly delete the pod.
|
||||
if err := f.DeletePodImmediately(pod.Namespace, pod.Name); err != nil {
|
||||
t.Logf("Last saw pod in state: %+v", podLast)
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("Force deleted pod: ", pod.Name)
|
||||
|
||||
// Wait for the delete event to be ACKed.
|
||||
if err := <-podCh; err != nil {
|
||||
t.Logf("Last saw pod in state: %+v", podLast)
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Give the provider some time to react to the MODIFIED/DELETED events before proceeding.
|
||||
time.Sleep(deleteGracePeriodForProvider)
|
||||
|
||||
// Grab the pods from the provider.
|
||||
pods, err = f.GetRunningPods()
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Make sure the "nginx-" pod DOES NOT exist in the slice of Pods anymore.
|
||||
assert.Assert(t, findPodInPods(pods, pod) != nil)
|
||||
|
||||
t.Logf("Pod ended as phase: %+v", podLast.Status.Phase)
|
||||
|
||||
}
|
||||
|
||||
// TestCreatePodWithOptionalInexistentSecrets tries to create a pod referencing optional, inexistent secrets.
|
||||
// It then verifies that the pod is created successfully.
|
||||
func TestCreatePodWithOptionalInexistentSecrets(t *testing.T) {
|
||||
// Create a pod with a single container referencing optional, inexistent secrets.
|
||||
pod, err := f.CreatePod(f.CreatePodObjectWithOptionalSecretKey(t.Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Delete the pod after the test finishes.
|
||||
defer func() {
|
||||
if err := f.DeletePodImmediately(pod.Namespace, pod.Name); err != nil && !apierrors.IsNotFound(err) {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for the pod to be reported as running and ready.
|
||||
if _, err := f.WaitUntilPodReady(pod.Namespace, pod.Name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for an event concerning the missing secret to be reported on the pod.
|
||||
if err := f.WaitUntilPodEventWithReason(pod, node.ReasonOptionalSecretNotFound); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Grab the pods from the provider.
|
||||
pods, err := f.GetRunningPods()
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Check if the pod exists in the slice of Pods.
|
||||
assert.NilError(t, findPodInPods(pods, pod))
|
||||
}
|
||||
|
||||
// TestCreatePodWithMandatoryInexistentSecrets tries to create a pod referencing inexistent secrets.
|
||||
// It then verifies that the pod is not created.
|
||||
func TestCreatePodWithMandatoryInexistentSecrets(t *testing.T) {
|
||||
// Create a pod with a single container referencing inexistent secrets.
|
||||
pod, err := f.CreatePod(f.CreatePodObjectWithMandatorySecretKey(t.Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Delete the pod after the test finishes.
|
||||
defer func() {
|
||||
if err := f.DeletePodImmediately(pod.Namespace, pod.Name); err != nil && !apierrors.IsNotFound(err) {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for an event concerning the missing secret to be reported on the pod.
|
||||
if err := f.WaitUntilPodEventWithReason(pod, node.ReasonMandatorySecretNotFound); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Grab the pods from the provider.
|
||||
pods, err := f.GetRunningPods()
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Check if the pod exists in the slice of PodStats.
|
||||
assert.Assert(t, findPodInPods(pods, pod) != nil)
|
||||
}
|
||||
|
||||
// TestCreatePodWithOptionalInexistentConfigMap tries to create a pod referencing optional, inexistent config map.
|
||||
// It then verifies that the pod is created successfully.
|
||||
func TestCreatePodWithOptionalInexistentConfigMap(t *testing.T) {
|
||||
// Create a pod with a single container referencing optional, inexistent config map.
|
||||
pod, err := f.CreatePod(f.CreatePodObjectWithOptionalConfigMapKey(t.Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Delete the pod after the test finishes.
|
||||
defer func() {
|
||||
if err := f.DeletePodImmediately(pod.Namespace, pod.Name); err != nil && !apierrors.IsNotFound(err) {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for the pod to be reported as running and ready.
|
||||
if _, err := f.WaitUntilPodReady(pod.Namespace, pod.Name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for an event concerning the missing config map to be reported on the pod.
|
||||
if err := f.WaitUntilPodEventWithReason(pod, node.ReasonOptionalConfigMapNotFound); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Grab the pods from the provider.
|
||||
pods, err := f.GetRunningPods()
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Check if the pod exists in the slice of PodStats.
|
||||
assert.NilError(t, findPodInPods(pods, pod))
|
||||
}
|
||||
|
||||
// TestCreatePodWithMandatoryInexistentConfigMap tries to create a pod referencing inexistent secrets.
|
||||
// It then verifies that the pod is not created.
|
||||
func TestCreatePodWithMandatoryInexistentConfigMap(t *testing.T) {
|
||||
// Create a pod with a single container referencing inexistent config map.
|
||||
pod, err := f.CreatePod(f.CreatePodObjectWithMandatoryConfigMapKey(t.Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Delete the pod after the test finishes.
|
||||
defer func() {
|
||||
if err := f.DeletePodImmediately(pod.Namespace, pod.Name); err != nil && !apierrors.IsNotFound(err) {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for an event concerning the missing config map to be reported on the pod.
|
||||
if err := f.WaitUntilPodEventWithReason(pod, node.ReasonMandatoryConfigMapNotFound); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Grab the pods from the provider.
|
||||
pods, err := f.GetRunningPods()
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Check if the pod exists in the slice of PodStats.
|
||||
assert.Assert(t, findPodInPods(pods, pod) != nil)
|
||||
}
|
||||
|
||||
// findPodInPodStats returns the index of the specified pod in the .pods field of the specified Summary object.
|
||||
// It returns an error if the specified pod is not found.
|
||||
func findPodInPodStats(summary *v1alpha1.Summary, pod *v1.Pod) (int, error) {
|
||||
for i, p := range summary.Pods {
|
||||
if p.PodRef.Namespace == pod.Namespace && p.PodRef.Name == pod.Name && string(p.PodRef.UID) == string(pod.UID) {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
return -1, fmt.Errorf("failed to find pod \"%s/%s\" in the slice of pod stats", pod.Namespace, pod.Name)
|
||||
}
|
||||
|
||||
// findPodInPodStats returns the index of the specified pod in the .pods field of the specified PodList object.
|
||||
// It returns error if the pod doesn't exist in the podlist
|
||||
func findPodInPods(pods *v1.PodList, pod *v1.Pod) error {
|
||||
for _, p := range pods.Items {
|
||||
if p.Namespace == pod.Namespace && p.Name == pod.Name && string(p.UID) == string(pod.UID) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("failed to find pod \"%s/%s\" in the slice of pod list", pod.Namespace, pod.Name)
|
||||
}
|
||||
@@ -75,7 +75,7 @@ func (f *Framework) CreatePodObjectWithOptionalSecretKey(testName string) *corev
|
||||
|
||||
// CreatePodObjectWithEnv creates a pod object whose name starts with "env-test-" and that uses the specified environment configuration for its first container.
|
||||
func (f *Framework) CreatePodObjectWithEnv(testName string, env []corev1.EnvVar) *corev1.Pod {
|
||||
pod := f.CreateDummyPodObjectWithPrefix(testName, "env-test-", "foo")
|
||||
pod := f.CreateDummyPodObjectWithPrefix(testName, "env-test", "foo")
|
||||
pod.Spec.Containers[0].Env = env
|
||||
return pod
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package framework
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
@@ -8,17 +10,19 @@ import (
|
||||
|
||||
// Framework encapsulates the configuration for the current run, and provides helper methods to be used during testing.
|
||||
type Framework struct {
|
||||
KubeClient kubernetes.Interface
|
||||
Namespace string
|
||||
NodeName string
|
||||
KubeClient kubernetes.Interface
|
||||
Namespace string
|
||||
NodeName string
|
||||
WatchTimeout time.Duration
|
||||
}
|
||||
|
||||
// NewTestingFramework returns a new instance of the testing framework.
|
||||
func NewTestingFramework(kubeconfig, namespace, nodeName string) *Framework {
|
||||
func NewTestingFramework(kubeconfig, namespace, nodeName string, watchTimeout time.Duration) *Framework {
|
||||
return &Framework{
|
||||
KubeClient: createKubeClient(kubeconfig),
|
||||
Namespace: namespace,
|
||||
NodeName: nodeName,
|
||||
KubeClient: createKubeClient(kubeconfig),
|
||||
Namespace: namespace,
|
||||
NodeName: nodeName,
|
||||
WatchTimeout: watchTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ func (f *Framework) WaitUntilNodeCondition(fn watch.ConditionFunc) error {
|
||||
}
|
||||
|
||||
// Watch for updates to the Pod resource until fn is satisfied, or until the timeout is reached.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultWatchTimeout)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), f.WatchTimeout)
|
||||
defer cancel()
|
||||
last, err := watch.UntilWithSync(ctx, lw, &corev1.Node{}, nil, fn)
|
||||
if err != nil {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -16,8 +15,6 @@ import (
|
||||
podutil "k8s.io/kubernetes/pkg/api/v1/pod"
|
||||
)
|
||||
|
||||
const defaultWatchTimeout = 2 * time.Minute
|
||||
|
||||
// CreateDummyPodObjectWithPrefix creates a dujmmy pod object using the specified prefix as the value of .metadata.generateName.
|
||||
// A variable number of strings can be provided.
|
||||
// For each one of these strings, a container that uses the string as its image will be appended to the pod.
|
||||
@@ -25,8 +22,7 @@ const defaultWatchTimeout = 2 * time.Minute
|
||||
func (f *Framework) CreateDummyPodObjectWithPrefix(testName string, prefix string, images ...string) *corev1.Pod {
|
||||
// Safe the test name
|
||||
if testName != "" {
|
||||
testName = strings.Replace(testName, "/", "-", -1)
|
||||
testName = strings.ToLower(testName)
|
||||
testName = stripParentTestName(strings.ToLower(testName))
|
||||
prefix = prefix + "-" + testName + "-"
|
||||
}
|
||||
enableServiceLink := false
|
||||
@@ -88,7 +84,7 @@ func (f *Framework) WaitUntilPodCondition(namespace, name string, fn watch.Condi
|
||||
},
|
||||
}
|
||||
// Watch for updates to the Pod resource until fn is satisfied, or until the timeout is reached.
|
||||
ctx, cfn := context.WithTimeout(context.Background(), defaultWatchTimeout)
|
||||
ctx, cfn := context.WithTimeout(context.Background(), f.WatchTimeout)
|
||||
defer cfn()
|
||||
last, err := watch.UntilWithSync(ctx, lw, &corev1.Pod{}, nil, fn)
|
||||
if err != nil {
|
||||
@@ -147,7 +143,7 @@ func (f *Framework) WaitUntilPodEventWithReason(pod *corev1.Pod, reason string)
|
||||
},
|
||||
}
|
||||
// Watch for updates to the Event resource until fn is satisfied, or until the timeout is reached.
|
||||
ctx, cfn := context.WithTimeout(context.Background(), defaultWatchTimeout)
|
||||
ctx, cfn := context.WithTimeout(context.Background(), f.WatchTimeout)
|
||||
defer cfn()
|
||||
last, err := watch.UntilWithSync(ctx, lw, &corev1.Event{}, nil, func(event watchapi.Event) (b bool, e error) {
|
||||
switch event.Type {
|
||||
@@ -184,3 +180,15 @@ func (f *Framework) GetRunningPods() (*corev1.PodList, error) {
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
// stripParentTestName strips out the parent's test name from the input (in the form of 'TestParent/TestChild').
|
||||
// Some test cases use their name as the pod name for testing purpose, and sometimes it might exceed 63
|
||||
// characters (Kubernetes's limit for pod name). This function ensures that we strip out the parent's
|
||||
// test name to decrease the length of the pod name
|
||||
func stripParentTestName(name string) string {
|
||||
parts := strings.Split(name, "/")
|
||||
if len(parts) == 1 {
|
||||
return parts[0]
|
||||
}
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ package e2e
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
vke2e "github.com/virtual-kubelet/virtual-kubelet/test/e2e"
|
||||
|
||||
"github.com/virtual-kubelet/virtual-kubelet/internal/test/e2e/framework"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -18,15 +18,9 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
// f is the testing framework used for running the test suite.
|
||||
f *framework.Framework
|
||||
|
||||
// kubeconfig is the path to the kubeconfig file to use when running the test suite outside a Kubernetes cluster.
|
||||
kubeconfig string
|
||||
// namespace is the name of the Kubernetes namespace to use for running the test suite (i.e. where to create pods).
|
||||
namespace string
|
||||
// nodeName is the name of the virtual-kubelet node to test.
|
||||
nodeName string
|
||||
namespace string
|
||||
nodeName string
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -36,17 +30,36 @@ func init() {
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// Set sane defaults in case no values (or empty ones) have been provided.
|
||||
// Provider-specific setup function
|
||||
func setup() error {
|
||||
fmt.Println("Setting up end-to-end test suite for mock provider...")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Provider-specific teardown function
|
||||
func teardown() error {
|
||||
fmt.Println("Tearing down end-to-end test suite for mock provider...")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Provider-specific shouldSkipTest function
|
||||
func shouldSkipTest(testName string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// TestEndToEnd creates and runs the end-to-end test suite for virtual kubelet
|
||||
func TestEndToEnd(t *testing.T) {
|
||||
setDefaults()
|
||||
// Create a new instance of the test framework targeting the specified node.
|
||||
f = framework.NewTestingFramework(kubeconfig, namespace, nodeName)
|
||||
// Wait for the virtual-kubelet pod to be ready.
|
||||
if _, err := f.WaitUntilPodReady(namespace, nodeName); err != nil {
|
||||
panic(err)
|
||||
config := vke2e.EndToEndTestSuiteConfig{
|
||||
Kubeconfig: kubeconfig,
|
||||
Namespace: namespace,
|
||||
NodeName: nodeName,
|
||||
Setup: setup,
|
||||
Teardown: teardown,
|
||||
ShouldSkipTest: shouldSkipTest,
|
||||
}
|
||||
// Run the test suite.
|
||||
os.Exit(m.Run())
|
||||
ts := vke2e.NewEndToEndTestSuite(config)
|
||||
ts.Run(t)
|
||||
}
|
||||
|
||||
// setDefaults sets sane defaults in case no values (or empty ones) have been provided.
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
// +build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gotest.tools/assert"
|
||||
is "gotest.tools/assert/cmp"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
watchapi "k8s.io/apimachinery/pkg/watch"
|
||||
)
|
||||
|
||||
// TestNodeCreateAfterDelete makes sure that a node is automatically recreated
|
||||
// if it is deleted while VK is running.
|
||||
func TestNodeCreateAfterDelete(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
podList, err := f.KubeClient.CoreV1().Pods(f.Namespace).List(metav1.ListOptions{
|
||||
FieldSelector: fields.OneTermEqualSelector("spec.nodeName", f.NodeName).String(),
|
||||
})
|
||||
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, is.Len(podList.Items, 0), "Kubernetes does not allow node deletion with dependent objects (pods) in existence: %v")
|
||||
|
||||
chErr := make(chan error, 1)
|
||||
|
||||
originalNode, err := f.GetNode()
|
||||
assert.NilError(t, err)
|
||||
|
||||
ctx, cancel = context.WithTimeout(ctx, time.Minute)
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
wait := func(e watchapi.Event) (bool, error) {
|
||||
err = ctx.Err()
|
||||
// Our timeout has expired
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
if e.Type == watchapi.Deleted || e.Type == watchapi.Error {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return originalNode.ObjectMeta.UID != e.Object.(*v1.Node).ObjectMeta.UID, nil
|
||||
}
|
||||
chErr <- f.WaitUntilNodeCondition(wait)
|
||||
}()
|
||||
|
||||
assert.NilError(t, f.DeleteNode())
|
||||
|
||||
select {
|
||||
case result := <-chErr:
|
||||
assert.NilError(t, result, "Did not observe new node object created after deletion")
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Test timed out while waiting for node object to be deleted / recreated")
|
||||
}
|
||||
}
|
||||
85
internal/test/suite/suite.go
Normal file
85
internal/test/suite/suite.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package suite
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestFunc defines the test function in a test case
|
||||
type TestFunc func(*testing.T)
|
||||
|
||||
// SetUpFunc sets up provider-specific resource in the test suite
|
||||
type SetUpFunc func() error
|
||||
|
||||
// TeardownFunc tears down provider-specific resources from the test suite
|
||||
type TeardownFunc func() error
|
||||
|
||||
// ShouldSkipTestFunc determines whether the test suite should skip certain tests
|
||||
type ShouldSkipTestFunc func(string) bool
|
||||
|
||||
// TestSuite contains methods that defines the lifecycle of a test suite
|
||||
type TestSuite interface {
|
||||
Setup()
|
||||
Teardown()
|
||||
}
|
||||
|
||||
// TestSkipper allows providers to skip certain tests
|
||||
type TestSkipper interface {
|
||||
ShouldSkipTest(string) bool
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
f TestFunc
|
||||
}
|
||||
|
||||
// Run runs tests registered in the test suite
|
||||
func Run(t *testing.T, ts TestSuite) {
|
||||
defer failOnPanic(t)
|
||||
|
||||
ts.Setup()
|
||||
defer ts.Teardown()
|
||||
|
||||
// The implementation below is based on https://github.com/stretchr/testify
|
||||
testFinder := reflect.TypeOf(ts)
|
||||
tests := []testCase{}
|
||||
for i := 0; i < testFinder.NumMethod(); i++ {
|
||||
method := testFinder.Method(i)
|
||||
if !isValidTestFunc(method) {
|
||||
continue
|
||||
}
|
||||
|
||||
test := testCase{
|
||||
name: method.Name,
|
||||
f: func(t *testing.T) {
|
||||
defer failOnPanic(t)
|
||||
if tSkipper, ok := ts.(TestSkipper); ok && tSkipper.ShouldSkipTest(method.Name) {
|
||||
t.Skipf("Skipped due to shouldSkipTest()")
|
||||
}
|
||||
method.Func.Call([]reflect.Value{reflect.ValueOf(ts), reflect.ValueOf(t)})
|
||||
},
|
||||
}
|
||||
tests = append(tests, test)
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, test.f)
|
||||
}
|
||||
}
|
||||
|
||||
// failOnPanic recovers panic occurred in the test suite and marks the test / test suite as failed
|
||||
func failOnPanic(t *testing.T) {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("%v\n%s", r, debug.Stack())
|
||||
}
|
||||
}
|
||||
|
||||
// isValidTestFunc determines whether or not a given method is a valid test function
|
||||
func isValidTestFunc(method reflect.Method) bool {
|
||||
return strings.HasPrefix(method.Name, "Test") && // Test function name must start with "Test",
|
||||
method.Type.NumIn() == 2 && // the number of function input should be 2 (*TestSuite ts and t *testing.T),
|
||||
method.Type.In(1) == reflect.TypeOf(&testing.T{}) &&
|
||||
method.Type.NumOut() == 0 // and the number of function output should be 0
|
||||
}
|
||||
126
internal/test/suite/suite_test.go
Normal file
126
internal/test/suite/suite_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package suite
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/assert"
|
||||
is "gotest.tools/assert/cmp"
|
||||
)
|
||||
|
||||
type basicTestSuite struct {
|
||||
setupCount int
|
||||
testFooCount int
|
||||
testBarCount int
|
||||
bazCount int
|
||||
testFooBarCount int
|
||||
testFooBazCount int
|
||||
testBarBazCount int
|
||||
teardownCount int
|
||||
testsRan []string
|
||||
}
|
||||
|
||||
func (bts *basicTestSuite) Setup() {
|
||||
bts.setupCount++
|
||||
}
|
||||
|
||||
func (bts *basicTestSuite) Teardown() {
|
||||
bts.teardownCount++
|
||||
}
|
||||
|
||||
func (bts *basicTestSuite) TestFoo(t *testing.T) {
|
||||
bts.testFooCount++
|
||||
bts.testsRan = append(bts.testsRan, t.Name())
|
||||
}
|
||||
|
||||
func (bts *basicTestSuite) TestBar(t *testing.T) {
|
||||
bts.testBarCount++
|
||||
bts.testsRan = append(bts.testsRan, t.Name())
|
||||
}
|
||||
|
||||
// Baz should not be executed by the test suite
|
||||
// because it does not have the prefix 'Test'
|
||||
func (bts *basicTestSuite) Baz(t *testing.T) {
|
||||
bts.bazCount++
|
||||
bts.testsRan = append(bts.testsRan, t.Name())
|
||||
}
|
||||
|
||||
// TestFooBar should not be executed by the test suite
|
||||
// because the number of function input is not 2 (*basicTestSuite and *testing.T)
|
||||
func (bts *basicTestSuite) TestFooBar() {
|
||||
bts.testFooBarCount++
|
||||
bts.testsRan = append(bts.testsRan, "TestFooBar")
|
||||
}
|
||||
|
||||
// TestFooBaz should not be executed by the test suite
|
||||
// because the number of function output is not 0
|
||||
func (bts *basicTestSuite) TestFooBaz(t *testing.T) error {
|
||||
bts.testFooBazCount++
|
||||
bts.testsRan = append(bts.testsRan, t.Name())
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestBarBaz should not be executed by the test suite
|
||||
// because the type of the function input is not *testing.T
|
||||
func (bts *basicTestSuite) TestBarBaz(t string) {
|
||||
bts.testBarBazCount++
|
||||
bts.testsRan = append(bts.testsRan, "TestBarBaz")
|
||||
}
|
||||
|
||||
func TestBasicTestSuite(t *testing.T) {
|
||||
bts := new(basicTestSuite)
|
||||
Run(t, bts)
|
||||
|
||||
assert.Equal(t, bts.setupCount, 1)
|
||||
assert.Equal(t, bts.testFooCount, 1)
|
||||
assert.Equal(t, bts.testBarCount, 1)
|
||||
assert.Equal(t, bts.teardownCount, 1)
|
||||
assert.Assert(t, is.Len(bts.testsRan, 2))
|
||||
assertTestsRan(t, bts.testsRan)
|
||||
assertNonTests(t, bts)
|
||||
}
|
||||
|
||||
type skipTestSuite struct {
|
||||
basicTestSuite
|
||||
skippedTestCount int
|
||||
}
|
||||
|
||||
func (sts *skipTestSuite) ShouldSkipTest(testName string) bool {
|
||||
if testName == "TestBar" {
|
||||
sts.skippedTestCount++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestSkipTest(t *testing.T) {
|
||||
sts := new(skipTestSuite)
|
||||
Run(t, sts)
|
||||
|
||||
assert.Equal(t, sts.setupCount, 1)
|
||||
assert.Equal(t, sts.testFooCount, 1)
|
||||
assert.Equal(t, sts.testBarCount, 0)
|
||||
assert.Equal(t, sts.teardownCount, 1)
|
||||
assert.Equal(t, sts.skippedTestCount, 1)
|
||||
assert.Assert(t, is.Len(sts.testsRan, 1))
|
||||
assertTestsRan(t, sts.testsRan)
|
||||
assertNonTests(t, &sts.basicTestSuite)
|
||||
}
|
||||
|
||||
func assertTestsRan(t *testing.T, testsRan []string) {
|
||||
for _, testRan := range testsRan {
|
||||
parts := strings.Split(testRan, "/")
|
||||
// Make sure that the name of the test has exactly one parent name and one subtest name
|
||||
assert.Assert(t, is.Len(parts, 2))
|
||||
// Check the parent test's name
|
||||
assert.Equal(t, parts[0], t.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// assertNonTests ensures that any malformed test functions are not run by the test suite
|
||||
func assertNonTests(t *testing.T, bts *basicTestSuite) {
|
||||
assert.Equal(t, bts.bazCount, 0)
|
||||
assert.Equal(t, bts.testFooBarCount, 0)
|
||||
assert.Equal(t, bts.testFooBazCount, 0)
|
||||
assert.Equal(t, bts.testBarBazCount, 0)
|
||||
}
|
||||
Reference in New Issue
Block a user