From e2dde20679b64caa7ad3d26b6765c1ae73956078 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Thu, 28 Apr 2016 15:26:04 -0700 Subject: [PATCH] feat(e2e): initial e2e test framework and runner --- Makefile | 7 ++++ test/e2e/command.go | 83 ++++++++++++++++++++++++++++++++++++++++++ test/e2e/helm.go | 73 +++++++++++++++++++++++++++++++++++++ test/e2e/helm_test.go | 78 +++++++++++++++++++++++++++++++++++++++ test/e2e/kubernetes.go | 61 +++++++++++++++++++++++++++++++ test/e2e/main_test.go | 12 ++++++ 6 files changed, 314 insertions(+) create mode 100644 test/e2e/command.go create mode 100644 test/e2e/helm.go create mode 100644 test/e2e/helm_test.go create mode 100644 test/e2e/kubernetes.go create mode 100644 test/e2e/main_test.go diff --git a/Makefile b/Makefile index 8bdf29859..54da6c508 100644 --- a/Makefile +++ b/Makefile @@ -58,6 +58,13 @@ test-unit: test-style: @scripts/validate-go.sh +.PHONY: test-e2e +test-e2e: TESTFLAGS += -v -tags=e2e +test-e2e: PKG = ./test/e2e +test-e2e: + @scripts/local-cluster.sh up + $(GO) test $(GOFLAGS) -run $(TESTS) $(PKG) $(TESTFLAGS) + .PHONY: clean clean: @rm -rf $(BINDIR) diff --git a/test/e2e/command.go b/test/e2e/command.go new file mode 100644 index 000000000..c01f18293 --- /dev/null +++ b/test/e2e/command.go @@ -0,0 +1,83 @@ +// build +e2e + +package e2e + +import ( + "bytes" + "fmt" + "os/exec" + "regexp" + "strings" + "testing" + "time" +) + +// Cmd provides helpers for command output +type Cmd struct { + t *testing.T + path string + args []string + ran bool + status error + stdout, stderr bytes.Buffer +} + +func (h *Cmd) String() string { + return fmt.Sprintf("%s %s", h.path, strings.Join(h.args, " ")) +} + +func (h *Cmd) exec() error { + cmd := exec.Command(h.path, h.args...) + h.stdout.Reset() + h.stderr.Reset() + cmd.Stdout = &h.stdout + cmd.Stderr = &h.stderr + + h.t.Logf("Executing command: %s", h) + start := time.Now() + h.status = cmd.Run() + h.t.Logf("Finished in %v", time.Since(start)) + + if h.stdout.Len() > 0 { + h.t.Logf("standard output:\n%s", h.stdout.String()) + } + if h.stderr.Len() > 0 { + h.t.Logf("standard error: %s\n", h.stderr.String()) + } + + h.ran = true + return h.status +} + +// Stdout returns standard output of the Cmd run as a string. +func (h *Cmd) Stdout() string { + if !h.ran { + h.t.Fatal("internal testsuite error: stdout called before run") + } + return h.stdout.String() +} + +// Stderr returns standard error of the Cmd run as a string. +func (h *Cmd) Stderr() string { + if !h.ran { + h.t.Fatal("internal testsuite error: stdout called before run") + } + return h.stderr.String() +} + +func (c *Cmd) Match(exp string) bool { + re := regexp.MustCompile(exp) + return re.MatchString(c.Stdout()) +} + +func (h *Cmd) StdoutContains(substring string) bool { + return strings.Contains(h.Stdout(), substring) +} + +func (h *Cmd) StderrContains(substring string) bool { + return strings.Contains(h.Stderr(), substring) +} + +func (h *Cmd) Contains(substring string) bool { + return h.StdoutContains(substring) || h.StderrContains(substring) +} diff --git a/test/e2e/helm.go b/test/e2e/helm.go new file mode 100644 index 000000000..54b995f19 --- /dev/null +++ b/test/e2e/helm.go @@ -0,0 +1,73 @@ +// build +e2e + +package e2e + +import ( + "testing" + "time" +) + +const ( + namespace = "helm" + apiProxy = "/api/v1/proxy/namespaces/" + namespace + "/services/manager-service:manager/" +) + +type HelmContext struct { + t *testing.T + Path string + Host string + Timeout time.Duration +} + +func NewHelmContext(t *testing.T) *HelmContext { + return &HelmContext{ + t: t, + Path: "../../bin/helm", + Timeout: time.Second * 20, + } +} + +func (h *HelmContext) MustRun(args ...string) *Cmd { + cmd := h.newCmd(args...) + if status := cmd.exec(); status != nil { + h.t.Errorf("helm %v failed unexpectedly: %v", args, status) + h.t.Errorf("%s", cmd.Stderr()) + h.t.FailNow() + } + return cmd +} + +func (h *HelmContext) Run(args ...string) *Cmd { + cmd := h.newCmd(args...) + cmd.exec() + return cmd +} + +func (h *HelmContext) RunFail(args ...string) *Cmd { + cmd := h.newCmd(args...) + if status := cmd.exec(); status == nil { + h.t.Fatalf("helm unexpected to fail: %v %v", args, status) + } + return cmd +} + +func (h *HelmContext) newCmd(args ...string) *Cmd { + //args = append([]string{"--host", h.Host}, args...) + return &Cmd{ + t: h.t, + path: h.Path, + args: args, + } +} + +func (h *HelmContext) Running() bool { + // FIXME tiller does not have a healthz endpoint + return true + + //endpoint := h.Host + "healthz" + //resp, err := http.Get(endpoint) + //if err != nil { + //h.t.Errorf("Could not GET %s: %s", endpoint, err) + //} + //return resp.StatusCode == 200 +} diff --git a/test/e2e/helm_test.go b/test/e2e/helm_test.go new file mode 100644 index 000000000..385657d94 --- /dev/null +++ b/test/e2e/helm_test.go @@ -0,0 +1,78 @@ +// build +e2e + +package e2e + +import ( + "flag" + "fmt" + "math/rand" + "testing" + "time" +) + +func init() { + rand.Seed(time.Now().Unix()) +} + +const ( + timeout = 180 * time.Second + poll = 2 * time.Second +) + +var ( + chart = flag.String("chart", "gs://kubernetes-charts-testing/redis-2.tgz", "Chart to deploy") + tillerImage = flag.String("tiller-image", "", "The full image name of the Docker image for resourcifier.") +) + +func logKubeEnv(k *KubeContext) { + config := k.Run("config", "view", "--flatten", "--minify").Stdout() + k.t.Logf("Kubernetes Environment\n%s", config) +} + +func TestHelm(t *testing.T) { + kube := NewKubeContext(t) + helm := NewHelmContext(t) + + logKubeEnv(kube) + + if !kube.Running() { + t.Fatal("Not connected to kubernetes") + } + t.Log(kube.Version()) + + if !isHelmRunning(kube) { + args := []string{"init"} + if *tillerImage != "" { + args = append(args, "-i", *tillerImage) + } + + helm.MustRun(args...) + + err := wait(func() bool { + return isHelmRunning(kube) + }) + + if err != nil { + t.Fatalf("could not install helm: %s", err) + } + } +} + +type conditionFunc func() bool + +func wait(fn conditionFunc) error { + for start := time.Now(); time.Since(start) < timeout; time.Sleep(poll) { + if fn() { + return nil + } + } + return fmt.Errorf("Polling timeout") +} + +func genName() string { + return fmt.Sprintf("e2e-%d", rand.Uint32()) +} + +func isHelmRunning(k *KubeContext) bool { + return k.Run("get", "pods", "--namespace=helm").StdoutContains("Running") +} diff --git a/test/e2e/kubernetes.go b/test/e2e/kubernetes.go new file mode 100644 index 000000000..a450e1f2c --- /dev/null +++ b/test/e2e/kubernetes.go @@ -0,0 +1,61 @@ +// build +e2e + +package e2e + +import ( + "strings" + "testing" +) + +const defaultKubectlPath = "kubectl" + +type KubeContext struct { + t *testing.T + Path string +} + +func NewKubeContext(t *testing.T) *KubeContext { + return &KubeContext{ + t: t, + Path: defaultKubectlPath, + } +} + +func (k *KubeContext) Run(args ...string) *Cmd { + cmd := k.newCmd(args...) + cmd.exec() + return cmd +} + +func (k *KubeContext) newCmd(args ...string) *Cmd { + return &Cmd{ + t: k.t, + path: k.Path, + args: args, + } +} + +func (k *KubeContext) getConfigValue(jsonpath string) string { + return strings.Replace(k.Run("config", "view", "--flatten=true", "--minify=true", "-o", "jsonpath="+jsonpath).Stdout(), "'", "", -1) +} + +func (k *KubeContext) Cluster() string { + return k.getConfigValue("'{.clusters[0].name}'") +} + +func (k *KubeContext) Server() string { + return k.getConfigValue("'{.clusters[0].cluster.server}'") +} + +func (k *KubeContext) CurrentContext() string { + return k.getConfigValue("'{.current-context}'") +} + +func (k *KubeContext) Running() bool { + err := k.Run("cluster-info").exec() + return err == nil +} + +func (k *KubeContext) Version() string { + return k.Run("version").Stdout() +} diff --git a/test/e2e/main_test.go b/test/e2e/main_test.go new file mode 100644 index 000000000..76793e638 --- /dev/null +++ b/test/e2e/main_test.go @@ -0,0 +1,12 @@ +// +build !e2e + +package e2e + +import ( + "os" + "testing" +) + +func TestMain(m *testing.M) { + os.Exit(0) +}