From dd0f17d7a53c4b0e5c5d86281ce507d6a2070798 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Wed, 4 Aug 2021 10:47:33 +0300 Subject: [PATCH] Refactor test helpers - move test helpers to main - add support for inline golden values - add test for `flux --version` Signed-off-by: Stefan Prodan --- cmd/flux/main_test.go | 158 ++++++++++++++++++ cmd/flux/testdata/trace/deployment.txt | 1 - .../trace/helmrelease-missing-git-ref.txt | 1 - cmd/flux/testdata/trace/helmrelease.txt | 1 - cmd/flux/testdata/trace/no-args.txt | 1 - cmd/flux/trace_test.go | 142 +--------------- 6 files changed, 161 insertions(+), 143 deletions(-) create mode 100644 cmd/flux/main_test.go delete mode 100644 cmd/flux/testdata/trace/no-args.txt diff --git a/cmd/flux/main_test.go b/cmd/flux/main_test.go new file mode 100644 index 00000000..0c9e7615 --- /dev/null +++ b/cmd/flux/main_test.go @@ -0,0 +1,158 @@ +package main + +import ( + "bufio" + "bytes" + "context" + "io" + "io/ioutil" + "os" + "testing" + + "github.com/fluxcd/flux2/internal/utils" + "github.com/google/go-cmp/cmp" + "github.com/mattn/go-shellwords" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + k8syaml "k8s.io/apimachinery/pkg/util/yaml" + "sigs.k8s.io/controller-runtime/pkg/client" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func init() { + // Ensure tests print consistent timestamps regardless of timezone + os.Setenv("TZ", "UTC") +} + +func readYamlObjects(objectFile string) ([]client.Object, error) { + obj, err := ioutil.ReadFile(objectFile) + if err != nil { + return nil, err + } + objects := []client.Object{} + reader := k8syaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(obj))) + for { + doc, err := reader.Read() + if err != nil { + if err == io.EOF { + break + } + } + unstructuredObj := &unstructured.Unstructured{} + decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(doc), len(doc)) + err = decoder.Decode(unstructuredObj) + if err != nil { + return nil, err + } + objects = append(objects, unstructuredObj) + } + return objects, nil +} + +// A KubeManager that can create objects that are subject to a test. +type fakeKubeManager struct { + fakeClient client.Client +} + +func (m *fakeKubeManager) NewClient(kubeconfig string, kubecontext string) (client.Client, error) { + return m.fakeClient, nil +} + +func (m *fakeKubeManager) CreateObjects(clientObjects []client.Object) error { + for _, obj := range clientObjects { + err := m.fakeClient.Create(context.Background(), obj) + if err != nil { + return err + } + } + return nil +} + +func NewFakeKubeManager() *fakeKubeManager { + c := fakeclient.NewClientBuilder().WithScheme(utils.NewScheme()).Build() + return &fakeKubeManager{ + fakeClient: c, + } +} + +// Run the command and return the captured output. +func executeCommand(cmd string) (string, error) { + args, err := shellwords.Parse(cmd) + if err != nil { + return "", err + } + + buf := new(bytes.Buffer) + + rootCmd.SetOut(buf) + rootCmd.SetErr(buf) + rootCmd.SetArgs(args) + + _, err = rootCmd.ExecuteC() + result := buf.String() + + return result, err +} + +// Structure used for each test to load objects into kubernetes, run +// commands and assert on the expected output. +type cmdTestCase struct { + // The command line arguments to test. + args string + // When true, the test expects the command to fail. + wantError bool + // String literal that contains the expected test output. + goldenValue string + // Filename that contains the expected test output. + goldenFile string + // Filename that contains yaml objects to load into Kubernetes + objectFile string +} + +func (cmd *cmdTestCase) runTestCmd(t *testing.T) { + km := NewFakeKubeManager() + rootCtx.kubeManager = km + + if cmd.objectFile != "" { + clientObjects, err := readYamlObjects(cmd.objectFile) + if err != nil { + t.Fatalf("Error loading yaml: '%v'", err) + } + err = km.CreateObjects(clientObjects) + if err != nil { + t.Fatalf("Error creating test objects: '%v'", err) + } + } + + actual, err := executeCommand(cmd.args) + if (err != nil) != cmd.wantError { + t.Fatalf("Expected error='%v', Got: %v", cmd.wantError, err) + } + if err != nil { + actual = err.Error() + } + + var expected string + if cmd.goldenValue != "" { + expected = cmd.goldenValue + } + if cmd.goldenFile != "" { + expectedOutput, err := ioutil.ReadFile(cmd.goldenFile) + if err != nil { + t.Fatalf("Error reading golden file: '%s'", err) + } + expected = string(expectedOutput) + } + + diff := cmp.Diff(expected, actual) + if diff != "" { + t.Errorf("Mismatch from '%s' (-want +got):\n%s", cmd.goldenFile, diff) + } +} + +func TestVersion(t *testing.T) { + cmd := cmdTestCase{ + args: "--version", + goldenValue: "flux version 0.0.0-dev.0\n", + } + cmd.runTestCmd(t) +} diff --git a/cmd/flux/testdata/trace/deployment.txt b/cmd/flux/testdata/trace/deployment.txt index 04ad9399..a73bcd2b 100644 --- a/cmd/flux/testdata/trace/deployment.txt +++ b/cmd/flux/testdata/trace/deployment.txt @@ -23,4 +23,3 @@ URL: https://stefanprodan.github.io/podinfo Revision: 8411f23d07d3701f0e96e7d9e503b7936d7e1d56 Status: Last reconciled at 2021-07-11 00:25:46 +0000 UTC Message: Fetched revision: 8411f23d07d3701f0e96e7d9e503b7936d7e1d56 - diff --git a/cmd/flux/testdata/trace/helmrelease-missing-git-ref.txt b/cmd/flux/testdata/trace/helmrelease-missing-git-ref.txt index f43f28cc..7eb18c50 100644 --- a/cmd/flux/testdata/trace/helmrelease-missing-git-ref.txt +++ b/cmd/flux/testdata/trace/helmrelease-missing-git-ref.txt @@ -16,4 +16,3 @@ URL: ssh://git@github.com/example/repo Revision: main/696f056df216eea4f9401adbee0ff744d4df390f Status: Last reconciled at 2021-07-20 00:48:16 +0000 UTC Message: Fetched revision: main/696f056df216eea4f9401adbee0ff744d4df390f - diff --git a/cmd/flux/testdata/trace/helmrelease.txt b/cmd/flux/testdata/trace/helmrelease.txt index ac075ae8..d2cd40d1 100644 --- a/cmd/flux/testdata/trace/helmrelease.txt +++ b/cmd/flux/testdata/trace/helmrelease.txt @@ -17,4 +17,3 @@ Branch: main Revision: main/696f056df216eea4f9401adbee0ff744d4df390f Status: Last reconciled at 2021-07-20 00:48:16 +0000 UTC Message: Fetched revision: main/696f056df216eea4f9401adbee0ff744d4df390f - diff --git a/cmd/flux/testdata/trace/no-args.txt b/cmd/flux/testdata/trace/no-args.txt deleted file mode 100644 index 4f71b995..00000000 --- a/cmd/flux/testdata/trace/no-args.txt +++ /dev/null @@ -1 +0,0 @@ -object name is required diff --git a/cmd/flux/trace_test.go b/cmd/flux/trace_test.go index e194be2a..d66a51aa 100644 --- a/cmd/flux/trace_test.go +++ b/cmd/flux/trace_test.go @@ -1,150 +1,14 @@ package main import ( - "bufio" - "bytes" - "context" - "io" - "io/ioutil" - "os" - "strings" "testing" - - "github.com/fluxcd/flux2/internal/utils" - "github.com/google/go-cmp/cmp" - shellwords "github.com/mattn/go-shellwords" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - k8syaml "k8s.io/apimachinery/pkg/util/yaml" - "sigs.k8s.io/controller-runtime/pkg/client" - fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" ) -func init() { - // Ensure tests print consistent timestamps regardless of timezone - os.Setenv("TZ", "UTC") -} - -func readYamlObjects(objectFile string) ([]client.Object, error) { - obj, err := ioutil.ReadFile(objectFile) - if err != nil { - return nil, err - } - objects := []client.Object{} - reader := k8syaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(obj))) - for { - doc, err := reader.Read() - if err != nil { - if err == io.EOF { - break - } - } - unstructuredObj := &unstructured.Unstructured{} - decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(doc), len(doc)) - err = decoder.Decode(unstructuredObj) - if err != nil { - return nil, err - } - objects = append(objects, unstructuredObj) - } - return objects, nil -} - -// A KubeManager that can create objects that are subject to a test. -type fakeKubeManager struct { - fakeClient client.Client -} - -func (m *fakeKubeManager) NewClient(kubeconfig string, kubecontext string) (client.Client, error) { - return m.fakeClient, nil -} - -func (m *fakeKubeManager) CreateObjects(clientObjects []client.Object) error { - for _, obj := range clientObjects { - err := m.fakeClient.Create(context.Background(), obj) - if err != nil { - return err - } - } - return nil -} - -func NewFakeKubeManager() *fakeKubeManager { - c := fakeclient.NewClientBuilder().WithScheme(utils.NewScheme()).Build() - return &fakeKubeManager{ - fakeClient: c, - } -} - -// Run the command and return the captured output. -func executeCommand(cmd string) (string, error) { - args, err := shellwords.Parse(cmd) - if err != nil { - return "", err - } - - buf := new(bytes.Buffer) - - rootCmd.SetOut(buf) - rootCmd.SetErr(buf) - rootCmd.SetArgs(args) - - _, err = rootCmd.ExecuteC() - result := buf.String() - - return result, err -} - -// Structure used for each test to load objects into kubernetes, run -// commands and assert on the expected output. -type cmdTestCase struct { - // The command line arguments to test. - args string - // When true, the test expects the command to fail. - wantError bool - // Filename that contains the expected test output. - goldenFile string - // Filename that contains yaml objects to load into Kubernetes - objectFile string -} - -func (cmd *cmdTestCase) runTestCmd(t *testing.T) { - km := NewFakeKubeManager() - rootCtx.kubeManager = km - - if cmd.objectFile != "" { - clientObjects, err := readYamlObjects(cmd.objectFile) - if err != nil { - t.Fatalf("Error loading yaml: '%v'", err) - } - err = km.CreateObjects(clientObjects) - if err != nil { - t.Fatalf("Error creating test objects: '%v'", err) - } - } - - actual, err := executeCommand(cmd.args) - if (err != nil) != cmd.wantError { - t.Fatalf("Expected error='%v', Got: %v", cmd.wantError, err) - } - if err != nil { - actual = err.Error() - } - contents, err := ioutil.ReadFile(cmd.goldenFile) - if err != nil { - t.Fatalf("Error reading golden file: '%s'", err) - } - expected := strings.TrimSuffix(string(contents), "\n") - diff := cmp.Diff(expected, actual) - if diff != "" { - t.Errorf("Mismatch from '%s' (-want +got):\n%s", cmd.goldenFile, diff) - } -} - func TestTraceNoArgs(t *testing.T) { cmd := cmdTestCase{ - args: "trace", - wantError: true, - goldenFile: "testdata/trace/no-args.txt", + args: "trace", + wantError: true, + goldenValue: "object name is required", } cmd.runTestCmd(t) }