From cd9001b557d3a44e4290c67f0804f915a8f5e699 Mon Sep 17 00:00:00 2001 From: Adam Ross Date: Wed, 25 Oct 2017 15:09:31 -0700 Subject: [PATCH 1/3] Initial test coverage in util package. --- .travis.yml | 1 + cli/testing/testing.go | 98 +++++++++++++++++++++++++++++ cli/util/docker.go | 14 +++-- cli/util/docker_test.go | 132 ++++++++++++++++++++++++++++++++++++++++ cli/util/logger_test.go | 19 ++++++ 5 files changed, 259 insertions(+), 5 deletions(-) create mode 100644 cli/testing/testing.go create mode 100644 cli/util/docker_test.go create mode 100644 cli/util/logger_test.go diff --git a/.travis.yml b/.travis.yml index 79dff23..f11e04a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ before_install: cd cli script: - "../scripts/test-go-fmt.sh" - "go run main.go" + - "go test github.com/phase2/rig/cli/util" notifications: flowdock: diff --git a/cli/testing/testing.go b/cli/testing/testing.go new file mode 100644 index 0000000..fe7fce8 --- /dev/null +++ b/cli/testing/testing.go @@ -0,0 +1,98 @@ +// Testing package provides helpers to facilitate rig testing. +// See additional documentation: https://gist.github.com/grayside/ffeb68fa342cecf1ec158c011cbd2ea3 +package testing + +import ( + "fmt" + "os" + "os/exec" + "strings" + "testing" +) + +// ExecMockSet provides a set of unique mocks. The key matches the remote execution script. +type ExecMockSet map[string]string +// ExecMockCollection is a map of ExecMockSets. The key is a category of mocks, such as +// "success" or "notfound". +type ExecMockCollection map[string]ExecMockSet + +var mock ExecMockCollection + +// SetMockValues provides an easy setter that allows the TestMain implementation +// of individual test files to preload the potential values to use. +func SetMockByType(namespace string, mockSet ExecMockSet) { + if mock == nil { + mock = make(ExecMockCollection) + } + mock[namespace] = mockSet +} + + +// TestMain is a special function that takes over handling the behavior of the the test runner `go test` +// generates to execute your code. I do not know if you can have one per file or one for a project's entire +// collection of tests. +// +// In the example below, we rely on an environment variable: `GO_TEST_MODE` to determine whether the +// testing process will behave normally (running all test and handling the result, done by default) or +// will behavior in a special manner because we have tailored the way an exec.Command() will execute +// to flow through this logic instead of what was originally intended. +// +// You may be wondering, why would we go to such an elaborate length to mock the result of the a shell +// execution? Well, if we directly interpolated the mocked value for the command, the resulting object +// would be a string, and not the expected structure the code might be looking for as a result of executing +// a remote command. +// +// To use this function, implement TestMain in your own class, then call: +// +// testing.MainTestProcess(m) +func MainTestProcess(m *testing.M) { + switch os.Getenv("GO_TEST_MODE") { + case "": + // Normal test mode. + os.Exit(m.Run()) + + case "echo": + // Outputs the arguments passed to the test runner. + // This will be the command that would have executed under normal runtime. + // This mode can be used to test that we can predict programmatically assembled command that would be executed. + fmt.Println(strings.Join(os.Args[1:], " ")) + + case "mock": + if mock != nil { + // Used the command that would be executed under normal runtime as the key to our mock value map and outputs the value. + // I am still researching how to adjust this overall pattern to centralize the code as test helpers but allow individual + // test files to supply their own mock. + fmt.Printf("%s", mock["success"][strings.Join(os.Args[1:], " ")]) + } + } +} + +// mockExecCommand uses fakeExecCommand to transform the intended remote executation +// into something controlled by the test runner, then adds an environment variable to +// the command so TestMain routes it to the mocking functionality. +func MockExecCommand(command string, args...string) *exec.Cmd { + cmd := fakeExecCommand(command, args...) + cmd.Env = append(cmd.Env, "GO_TEST_MODE=mock") + return cmd +} + +// echoExecCommand uses fakeExecCommand to transform the intended remote executation +// into something controlled by the test runner, then adds an environment variable to +// the command so TestMain routes it to the command echo functionality. +func EchoExecCommand(command string, args...string) *exec.Cmd { + cmd := fakeExecCommand(command, args...) + cmd.Env = append(cmd.Env, "GO_TEST_MODE=echo") + return cmd +} + +// fakeExecCommand creates a new reference to an exec.Cmd object which has been transformed +// to use the supplied parameters as arguments to be submitted to our test runner binary. +// It should never be used directly. +func fakeExecCommand(command string, args...string) *exec.Cmd { + testArgs := []string{command} + testArgs = append(testArgs, args...) + cmd := exec.Command(os.Args[0], testArgs...) + cmd.Env = []string{} + + return cmd +} diff --git a/cli/util/docker.go b/cli/util/docker.go index fa7bf44..404ded8 100644 --- a/cli/util/docker.go +++ b/cli/util/docker.go @@ -9,8 +9,12 @@ import ( "github.com/hashicorp/go-version" ) +// Allow testing code to override the behavior of exec.Command or modify the +// resulting exec.Cmd object. +var execCommand = exec.Command + func GetRawCurrentDockerVersion() string { - output, _ := exec.Command("docker", "--version").Output() + output, _ := execCommand("docker", "--version").Output() re := regexp.MustCompile("Docker version (.*),") return re.FindAllStringSubmatch(string(output), -1)[0][1] } @@ -21,14 +25,14 @@ func GetCurrentDockerVersion() *version.Version { } func GetDockerClientApiVersion() *version.Version { - output, _ := exec.Command("docker", "version", "--format", "{{.Client.APIVersion}}").Output() + output, _ := execCommand("docker", "version", "--format", "{{.Client.APIVersion}}").Output() re := regexp.MustCompile("^([\\d|\\.]+)") versionNumber := re.FindAllStringSubmatch(string(output), -1)[0][1] return version.Must(version.NewVersion(versionNumber)) } func GetDockerServerApiVersion(name string) (*version.Version, error) { - output, err := exec.Command("docker-machine", "ssh", name, "docker version --format {{.Server.APIVersion}}").Output() + output, err := execCommand("docker-machine", "ssh", name, "docker version --format {{.Server.APIVersion}}").Output() if err != nil { return nil, err } @@ -36,7 +40,7 @@ func GetDockerServerApiVersion(name string) (*version.Version, error) { } func GetDockerServerMinApiVersion(name string) (*version.Version, error) { - output, err := exec.Command("docker-machine", "ssh", name, "docker version --format {{.Server.MinAPIVersion}}").Output() + output, err := execCommand("docker-machine", "ssh", name, "docker version --format {{.Server.MinAPIVersion}}").Output() if err != nil { return nil, err } @@ -45,7 +49,7 @@ func GetDockerServerMinApiVersion(name string) (*version.Version, error) { // Determine the age of the Docker Image and whether the image is older than the designated timestamp. func ImageOlderThan(image string, elapsed_seconds float64) (bool, float64, error) { - output, err := exec.Command("docker", "inspect", "--format", "{{.Created}}", image).Output() + output, err := execCommand("docker", "inspect", "--format", "{{.Created}}", image).Output() if err != nil { return false, 0, err } diff --git a/cli/util/docker_test.go b/cli/util/docker_test.go new file mode 100644 index 0000000..968d2b6 --- /dev/null +++ b/cli/util/docker_test.go @@ -0,0 +1,132 @@ +package util + +import ( + "testing" + + rigtest "github.com/phase2/rig/cli/testing" +) + +// mock provides mock values to use as lookup responses to functions we will execute in our production code. +// The idea is to use the command as a lookup key to the result it might generate. +// Currently it only supports a single value, in the future this may be split into multiple maps for different, +// generic classes of success and failure. We cannot use multiple values for entries in this map because each response +// is expected to be a string that an executed command would return to Stdout. +var mockSet = rigtest.ExecMockSet{ + "docker --version": "Docker version 17.09.0-ce, build afdb6d4", + "docker-machine ssh gastropod docker version --format {{.Server.APIVersion}}": "1.30", + "docker version --format {{.Client.APIVersion}}": "1.30", + "docker-machine ssh gastropod docker version --format {{.Server.MinAPIVersion}}": "1.12", + "docker inspect --format {{.Created}} outrigger/dust": "2017-09-18T21:43:00.565978065Z", +} + +func TestMain(m *testing.M) { + rigtest.SetMockByType("success", mockSet) + rigtest.MainTestProcess(m) +} + +// TestGetRawCurrentDockerVersion confirms successful Docker version extraction. +func TestGetRawCurrentDockerVersion(t *testing.T) { + // In case some other functionality has swapped out this value, we will store + // it explicitly rather than assume it is exec.Command. + stashCommand := execCommand + // Re-define execCommand so our runtime code executes using the mocking functionality. + // I thought execCommand would be a private variable in file scope, apparently sharing the package + // is enough to access and manipulate it. Or perhaps test functions have special scope rules? + execCommand = rigtest.MockExecCommand + // Put back the original behavior after we are done with this test function. + defer func(){ execCommand = stashCommand }() + // Run the code under test. + out := GetRawCurrentDockerVersion() + + // Implement our assertion. + expected := "17.09.0-ce" + if out != expected { + t.Errorf("GetRawCurrentDockerVersion: Expected %q, Actual %q", expected, out) + } +} + +// TestGetCurrentDockerVersion confirms successful processing of Docker version into version object. +// For more thoroughly commented exec wrangling details see TestGetRawCurrentDockerVersion. +func TestGetCurrentDockerVersion(t *testing.T) { + stashCommand := execCommand + execCommand = rigtest.MockExecCommand + defer func(){ execCommand = stashCommand }() + version, err := GetDockerServerApiVersion("gastropod") + + if err != nil { + t.Errorf("GetDockerServerApiVersion: %v", err) + } + + expected := "1.30.0" + if version.String() != expected { + t.Errorf("GetDockerServerApiVersion: Expected %q, Actual %q", expected, version) + } +} + +// TestGetDockerServerApiVersion confirms successful Docker client version extraction. +// For more thoroughly commented exec wrangling details see TestGetRawCurrentDockerVersion. +func TestGetDockerClientApiVersion(t *testing.T) { + stashCommand := execCommand + execCommand = rigtest.MockExecCommand + defer func(){ execCommand = stashCommand }() + version := GetDockerClientApiVersion() + + expected := "1.30.0" + if version.String() != expected { + t.Errorf("GetDockerClientApiVersion: Expected %q, Actual %q", expected, version) + } +} + +// TestGetDockerServerApiVersion confirms successful Docker server version extraction. +// For more thoroughly commented exec wrangling details see TestGetRawCurrentDockerVersion. +func TestGetDockerServerApiVersion(t *testing.T) { + stashCommand := execCommand + execCommand = rigtest.MockExecCommand + defer func(){ execCommand = stashCommand }() + version, err := GetDockerServerApiVersion("gastropod") + + if err != nil { + t.Errorf("GetDockerServerApiVersion: %v", err) + } + + expected := "1.30.0" + if version.String() != expected { + t.Errorf("GetDockerServerApiVersion: Expected %q, Actual %q", expected, version) + } +} + +// TestGetDockerServerMinApiVersion confirms successful Docker minimum API compatibility version. +// For more thoroughly commented exec wrangling details see TestGetRawCurrentDockerVersion. +func TestGetDockerServerMinApiVersion(t *testing.T) { + stashCommand := execCommand + execCommand = rigtest.MockExecCommand + defer func(){ execCommand = stashCommand }() + version, err := GetDockerServerMinApiVersion("gastropod") + + if err != nil { + t.Errorf("GetDockerServerMinApiVersion: %v", err) + } + + expected := "1.12.0" + if version.String() != expected { + t.Errorf("GetDockerServerMinApiVersion: Expected %q, Actual %q", expected, version) + } +} + +// TestImageOlderThan confirms image age evaluation. +// For more thoroughly commented exec wrangling details see TestGetRawCurrentDockerVersion. +// @TODO identify how to mock the current time so we can test this more completely. +func TestImageOlderThan(t *testing.T) { + stashCommand := execCommand + execCommand = rigtest.MockExecCommand + defer func(){ execCommand = stashCommand }() + older, _, err := ImageOlderThan("outrigger/dust", 86400*30) + + if err != nil { + t.Errorf("ImageOlderThan: %v", err) + } + + if !older { + t.Errorf("ImageOlderThan: Image is older than 30 days ago but reporting as newer.") + } +} diff --git a/cli/util/logger_test.go b/cli/util/logger_test.go new file mode 100644 index 0000000..b3bc5d4 --- /dev/null +++ b/cli/util/logger_test.go @@ -0,0 +1,19 @@ +package util + +import ( + "testing" +) + +func TestLoggerInit(t *testing.T) { + LoggerInit(false) + logger := Logger() + if logger.IsVerbose { + t.Error("Logger initialized in non-Verbose mode.") + } + + LoggerInit(true) + logger = Logger() + if !logger.IsVerbose { + t.Error("Logger initialized in Verbose mode.") + } +} From 3a51e301ed587ba64b239560eea964dd94e407b7 Mon Sep 17 00:00:00 2001 From: Adam Ross Date: Wed, 25 Oct 2017 15:13:02 -0700 Subject: [PATCH 2/3] Fix code formatting. --- cli/testing/testing.go | 82 +++++++++---------- cli/util/docker_test.go | 170 ++++++++++++++++++++-------------------- cli/util/logger_test.go | 22 +++--- 3 files changed, 137 insertions(+), 137 deletions(-) diff --git a/cli/testing/testing.go b/cli/testing/testing.go index fe7fce8..b6d0733 100644 --- a/cli/testing/testing.go +++ b/cli/testing/testing.go @@ -3,15 +3,16 @@ package testing import ( - "fmt" - "os" - "os/exec" - "strings" - "testing" + "fmt" + "os" + "os/exec" + "strings" + "testing" ) // ExecMockSet provides a set of unique mocks. The key matches the remote execution script. type ExecMockSet map[string]string + // ExecMockCollection is a map of ExecMockSets. The key is a category of mocks, such as // "success" or "notfound". type ExecMockCollection map[string]ExecMockSet @@ -21,13 +22,12 @@ var mock ExecMockCollection // SetMockValues provides an easy setter that allows the TestMain implementation // of individual test files to preload the potential values to use. func SetMockByType(namespace string, mockSet ExecMockSet) { - if mock == nil { - mock = make(ExecMockCollection) - } - mock[namespace] = mockSet + if mock == nil { + mock = make(ExecMockCollection) + } + mock[namespace] = mockSet } - // TestMain is a special function that takes over handling the behavior of the the test runner `go test` // generates to execute your code. I do not know if you can have one per file or one for a project's entire // collection of tests. @@ -46,53 +46,53 @@ func SetMockByType(namespace string, mockSet ExecMockSet) { // // testing.MainTestProcess(m) func MainTestProcess(m *testing.M) { - switch os.Getenv("GO_TEST_MODE") { - case "": - // Normal test mode. - os.Exit(m.Run()) + switch os.Getenv("GO_TEST_MODE") { + case "": + // Normal test mode. + os.Exit(m.Run()) - case "echo": - // Outputs the arguments passed to the test runner. - // This will be the command that would have executed under normal runtime. - // This mode can be used to test that we can predict programmatically assembled command that would be executed. - fmt.Println(strings.Join(os.Args[1:], " ")) + case "echo": + // Outputs the arguments passed to the test runner. + // This will be the command that would have executed under normal runtime. + // This mode can be used to test that we can predict programmatically assembled command that would be executed. + fmt.Println(strings.Join(os.Args[1:], " ")) - case "mock": - if mock != nil { - // Used the command that would be executed under normal runtime as the key to our mock value map and outputs the value. - // I am still researching how to adjust this overall pattern to centralize the code as test helpers but allow individual - // test files to supply their own mock. - fmt.Printf("%s", mock["success"][strings.Join(os.Args[1:], " ")]) - } - } + case "mock": + if mock != nil { + // Used the command that would be executed under normal runtime as the key to our mock value map and outputs the value. + // I am still researching how to adjust this overall pattern to centralize the code as test helpers but allow individual + // test files to supply their own mock. + fmt.Printf("%s", mock["success"][strings.Join(os.Args[1:], " ")]) + } + } } // mockExecCommand uses fakeExecCommand to transform the intended remote executation // into something controlled by the test runner, then adds an environment variable to // the command so TestMain routes it to the mocking functionality. -func MockExecCommand(command string, args...string) *exec.Cmd { - cmd := fakeExecCommand(command, args...) - cmd.Env = append(cmd.Env, "GO_TEST_MODE=mock") - return cmd +func MockExecCommand(command string, args ...string) *exec.Cmd { + cmd := fakeExecCommand(command, args...) + cmd.Env = append(cmd.Env, "GO_TEST_MODE=mock") + return cmd } // echoExecCommand uses fakeExecCommand to transform the intended remote executation // into something controlled by the test runner, then adds an environment variable to // the command so TestMain routes it to the command echo functionality. -func EchoExecCommand(command string, args...string) *exec.Cmd { - cmd := fakeExecCommand(command, args...) - cmd.Env = append(cmd.Env, "GO_TEST_MODE=echo") - return cmd +func EchoExecCommand(command string, args ...string) *exec.Cmd { + cmd := fakeExecCommand(command, args...) + cmd.Env = append(cmd.Env, "GO_TEST_MODE=echo") + return cmd } // fakeExecCommand creates a new reference to an exec.Cmd object which has been transformed // to use the supplied parameters as arguments to be submitted to our test runner binary. // It should never be used directly. -func fakeExecCommand(command string, args...string) *exec.Cmd { - testArgs := []string{command} - testArgs = append(testArgs, args...) - cmd := exec.Command(os.Args[0], testArgs...) - cmd.Env = []string{} +func fakeExecCommand(command string, args ...string) *exec.Cmd { + testArgs := []string{command} + testArgs = append(testArgs, args...) + cmd := exec.Command(os.Args[0], testArgs...) + cmd.Env = []string{} - return cmd + return cmd } diff --git a/cli/util/docker_test.go b/cli/util/docker_test.go index 968d2b6..25def46 100644 --- a/cli/util/docker_test.go +++ b/cli/util/docker_test.go @@ -1,9 +1,9 @@ package util import ( - "testing" + "testing" - rigtest "github.com/phase2/rig/cli/testing" + rigtest "github.com/phase2/rig/cli/testing" ) // mock provides mock values to use as lookup responses to functions we will execute in our production code. @@ -13,120 +13,120 @@ import ( // is expected to be a string that an executed command would return to Stdout. var mockSet = rigtest.ExecMockSet{ "docker --version": "Docker version 17.09.0-ce, build afdb6d4", - "docker-machine ssh gastropod docker version --format {{.Server.APIVersion}}": "1.30", - "docker version --format {{.Client.APIVersion}}": "1.30", - "docker-machine ssh gastropod docker version --format {{.Server.MinAPIVersion}}": "1.12", - "docker inspect --format {{.Created}} outrigger/dust": "2017-09-18T21:43:00.565978065Z", + "docker-machine ssh gastropod docker version --format {{.Server.APIVersion}}": "1.30", + "docker version --format {{.Client.APIVersion}}": "1.30", + "docker-machine ssh gastropod docker version --format {{.Server.MinAPIVersion}}": "1.12", + "docker inspect --format {{.Created}} outrigger/dust": "2017-09-18T21:43:00.565978065Z", } func TestMain(m *testing.M) { - rigtest.SetMockByType("success", mockSet) - rigtest.MainTestProcess(m) + rigtest.SetMockByType("success", mockSet) + rigtest.MainTestProcess(m) } // TestGetRawCurrentDockerVersion confirms successful Docker version extraction. func TestGetRawCurrentDockerVersion(t *testing.T) { - // In case some other functionality has swapped out this value, we will store - // it explicitly rather than assume it is exec.Command. - stashCommand := execCommand - // Re-define execCommand so our runtime code executes using the mocking functionality. - // I thought execCommand would be a private variable in file scope, apparently sharing the package - // is enough to access and manipulate it. Or perhaps test functions have special scope rules? - execCommand = rigtest.MockExecCommand - // Put back the original behavior after we are done with this test function. - defer func(){ execCommand = stashCommand }() - // Run the code under test. - out := GetRawCurrentDockerVersion() - - // Implement our assertion. - expected := "17.09.0-ce" - if out != expected { - t.Errorf("GetRawCurrentDockerVersion: Expected %q, Actual %q", expected, out) - } + // In case some other functionality has swapped out this value, we will store + // it explicitly rather than assume it is exec.Command. + stashCommand := execCommand + // Re-define execCommand so our runtime code executes using the mocking functionality. + // I thought execCommand would be a private variable in file scope, apparently sharing the package + // is enough to access and manipulate it. Or perhaps test functions have special scope rules? + execCommand = rigtest.MockExecCommand + // Put back the original behavior after we are done with this test function. + defer func() { execCommand = stashCommand }() + // Run the code under test. + out := GetRawCurrentDockerVersion() + + // Implement our assertion. + expected := "17.09.0-ce" + if out != expected { + t.Errorf("GetRawCurrentDockerVersion: Expected %q, Actual %q", expected, out) + } } // TestGetCurrentDockerVersion confirms successful processing of Docker version into version object. // For more thoroughly commented exec wrangling details see TestGetRawCurrentDockerVersion. func TestGetCurrentDockerVersion(t *testing.T) { - stashCommand := execCommand - execCommand = rigtest.MockExecCommand - defer func(){ execCommand = stashCommand }() - version, err := GetDockerServerApiVersion("gastropod") - - if err != nil { - t.Errorf("GetDockerServerApiVersion: %v", err) - } - - expected := "1.30.0" - if version.String() != expected { - t.Errorf("GetDockerServerApiVersion: Expected %q, Actual %q", expected, version) - } + stashCommand := execCommand + execCommand = rigtest.MockExecCommand + defer func() { execCommand = stashCommand }() + version, err := GetDockerServerApiVersion("gastropod") + + if err != nil { + t.Errorf("GetDockerServerApiVersion: %v", err) + } + + expected := "1.30.0" + if version.String() != expected { + t.Errorf("GetDockerServerApiVersion: Expected %q, Actual %q", expected, version) + } } // TestGetDockerServerApiVersion confirms successful Docker client version extraction. // For more thoroughly commented exec wrangling details see TestGetRawCurrentDockerVersion. func TestGetDockerClientApiVersion(t *testing.T) { - stashCommand := execCommand - execCommand = rigtest.MockExecCommand - defer func(){ execCommand = stashCommand }() - version := GetDockerClientApiVersion() - - expected := "1.30.0" - if version.String() != expected { - t.Errorf("GetDockerClientApiVersion: Expected %q, Actual %q", expected, version) - } + stashCommand := execCommand + execCommand = rigtest.MockExecCommand + defer func() { execCommand = stashCommand }() + version := GetDockerClientApiVersion() + + expected := "1.30.0" + if version.String() != expected { + t.Errorf("GetDockerClientApiVersion: Expected %q, Actual %q", expected, version) + } } // TestGetDockerServerApiVersion confirms successful Docker server version extraction. // For more thoroughly commented exec wrangling details see TestGetRawCurrentDockerVersion. func TestGetDockerServerApiVersion(t *testing.T) { - stashCommand := execCommand - execCommand = rigtest.MockExecCommand - defer func(){ execCommand = stashCommand }() - version, err := GetDockerServerApiVersion("gastropod") - - if err != nil { - t.Errorf("GetDockerServerApiVersion: %v", err) - } - - expected := "1.30.0" - if version.String() != expected { - t.Errorf("GetDockerServerApiVersion: Expected %q, Actual %q", expected, version) - } + stashCommand := execCommand + execCommand = rigtest.MockExecCommand + defer func() { execCommand = stashCommand }() + version, err := GetDockerServerApiVersion("gastropod") + + if err != nil { + t.Errorf("GetDockerServerApiVersion: %v", err) + } + + expected := "1.30.0" + if version.String() != expected { + t.Errorf("GetDockerServerApiVersion: Expected %q, Actual %q", expected, version) + } } // TestGetDockerServerMinApiVersion confirms successful Docker minimum API compatibility version. // For more thoroughly commented exec wrangling details see TestGetRawCurrentDockerVersion. func TestGetDockerServerMinApiVersion(t *testing.T) { - stashCommand := execCommand - execCommand = rigtest.MockExecCommand - defer func(){ execCommand = stashCommand }() - version, err := GetDockerServerMinApiVersion("gastropod") - - if err != nil { - t.Errorf("GetDockerServerMinApiVersion: %v", err) - } - - expected := "1.12.0" - if version.String() != expected { - t.Errorf("GetDockerServerMinApiVersion: Expected %q, Actual %q", expected, version) - } + stashCommand := execCommand + execCommand = rigtest.MockExecCommand + defer func() { execCommand = stashCommand }() + version, err := GetDockerServerMinApiVersion("gastropod") + + if err != nil { + t.Errorf("GetDockerServerMinApiVersion: %v", err) + } + + expected := "1.12.0" + if version.String() != expected { + t.Errorf("GetDockerServerMinApiVersion: Expected %q, Actual %q", expected, version) + } } // TestImageOlderThan confirms image age evaluation. // For more thoroughly commented exec wrangling details see TestGetRawCurrentDockerVersion. // @TODO identify how to mock the current time so we can test this more completely. func TestImageOlderThan(t *testing.T) { - stashCommand := execCommand - execCommand = rigtest.MockExecCommand - defer func(){ execCommand = stashCommand }() - older, _, err := ImageOlderThan("outrigger/dust", 86400*30) - - if err != nil { - t.Errorf("ImageOlderThan: %v", err) - } - - if !older { - t.Errorf("ImageOlderThan: Image is older than 30 days ago but reporting as newer.") - } + stashCommand := execCommand + execCommand = rigtest.MockExecCommand + defer func() { execCommand = stashCommand }() + older, _, err := ImageOlderThan("outrigger/dust", 86400*30) + + if err != nil { + t.Errorf("ImageOlderThan: %v", err) + } + + if !older { + t.Errorf("ImageOlderThan: Image is older than 30 days ago but reporting as newer.") + } } diff --git a/cli/util/logger_test.go b/cli/util/logger_test.go index b3bc5d4..c4097ab 100644 --- a/cli/util/logger_test.go +++ b/cli/util/logger_test.go @@ -1,19 +1,19 @@ package util import ( - "testing" + "testing" ) func TestLoggerInit(t *testing.T) { - LoggerInit(false) - logger := Logger() - if logger.IsVerbose { - t.Error("Logger initialized in non-Verbose mode.") - } + LoggerInit(false) + logger := Logger() + if logger.IsVerbose { + t.Error("Logger initialized in non-Verbose mode.") + } - LoggerInit(true) - logger = Logger() - if !logger.IsVerbose { - t.Error("Logger initialized in Verbose mode.") - } + LoggerInit(true) + logger = Logger() + if !logger.IsVerbose { + t.Error("Logger initialized in Verbose mode.") + } } From 28cbe32f87f2d5036921055671db08c716784b6f Mon Sep 17 00:00:00 2001 From: Adam Ross Date: Thu, 26 Oct 2017 20:07:31 -0700 Subject: [PATCH 3/3] Expand coverage, address single TestMain, add assert helpers. --- cli/testing/assert.go | 38 ++++++++++++ cli/testing/testing.go | 32 ++++++++-- cli/util/docker.go | 12 ++-- cli/util/docker_test.go | 114 ++++++++++++------------------------ cli/util/logger_test.go | 21 ++++--- cli/util/shell_exec_test.go | 18 ++++++ cli/util/util_test.go | 14 +++++ 7 files changed, 153 insertions(+), 96 deletions(-) create mode 100644 cli/testing/assert.go create mode 100644 cli/util/shell_exec_test.go create mode 100644 cli/util/util_test.go diff --git a/cli/testing/assert.go b/cli/testing/assert.go new file mode 100644 index 0000000..fe329f4 --- /dev/null +++ b/cli/testing/assert.go @@ -0,0 +1,38 @@ +// Code in this file originally copied from https://github.com/benbjohnson/testing +// @see https://medium.com/@benbjohnson/structuring-tests-in-go-46ddee7a25c +package testing + +import ( + "fmt" + "path/filepath" + "reflect" + "runtime" + "testing" +) + +// assert fails the test if the condition is false. +func Assert(tb testing.TB, condition bool, msg string, v ...interface{}) { + if !condition { + _, file, line, _ := runtime.Caller(1) + fmt.Printf("\033[31m%s:%d: "+msg+"\033[39m\n\n", append([]interface{}{filepath.Base(file), line}, v...)...) + tb.FailNow() + } +} + +// ok fails the test if an err is not nil. +func Ok(tb testing.TB, err error) { + if err != nil { + _, file, line, _ := runtime.Caller(1) + fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error()) + tb.FailNow() + } +} + +// equals fails the test if exp is not equal to act. +func Equals(tb testing.TB, exp, act interface{}) { + if !reflect.DeepEqual(exp, act) { + _, file, line, _ := runtime.Caller(1) + fmt.Printf("\033[31m%s:%d:\n\n\texpected: %#v\n\n\tactual: %#v\033[39m\n\n", filepath.Base(file), line, exp, act) + tb.FailNow() + } +} diff --git a/cli/testing/testing.go b/cli/testing/testing.go index b6d0733..e299b1d 100644 --- a/cli/testing/testing.go +++ b/cli/testing/testing.go @@ -57,6 +57,12 @@ func MainTestProcess(m *testing.M) { // This mode can be used to test that we can predict programmatically assembled command that would be executed. fmt.Println(strings.Join(os.Args[1:], " ")) + case "succeed": + os.Exit(0) + + case "fail": + os.Exit(42) + case "mock": if mock != nil { // Used the command that would be executed under normal runtime as the key to our mock value map and outputs the value. @@ -67,24 +73,42 @@ func MainTestProcess(m *testing.M) { } } -// mockExecCommand uses fakeExecCommand to transform the intended remote executation +// MockExecCommand uses fakeExecCommand to transform the intended remote execution // into something controlled by the test runner, then adds an environment variable to -// the command so TestMain routes it to the mocking functionality. +// the command so TestMain routes it to the command "mock" functionality. func MockExecCommand(command string, args ...string) *exec.Cmd { cmd := fakeExecCommand(command, args...) cmd.Env = append(cmd.Env, "GO_TEST_MODE=mock") return cmd } -// echoExecCommand uses fakeExecCommand to transform the intended remote executation +// EchoExecCommand uses fakeExecCommand to transform the intended remote execution // into something controlled by the test runner, then adds an environment variable to -// the command so TestMain routes it to the command echo functionality. +// the command so TestMain routes it to the command "echo" functionality. func EchoExecCommand(command string, args ...string) *exec.Cmd { cmd := fakeExecCommand(command, args...) cmd.Env = append(cmd.Env, "GO_TEST_MODE=echo") return cmd } +// SucceedExecCommand uses fakeExecCommand to transform the intended remote execution +// into something controlled by the test runner, then adds an environment variable to +// the command so TestMain routes it to the command "success" functionality. +func SuccessExecCommand(command string, args ...string) *exec.Cmd { + cmd := fakeExecCommand(command, args...) + cmd.Env = append(cmd.Env, "GO_TEST_MODE=success") + return cmd +} + +// FailExecCommand uses fakeExecCommand to transform the intended remote execution +// into something controlled by the test runner, then adds an environment variable to +// the command so TestMain routes it to the command "fail" functionality. +func FailExecCommand(command string, args ...string) *exec.Cmd { + cmd := fakeExecCommand(command, args...) + cmd.Env = append(cmd.Env, "GO_TEST_MODE=fail") + return cmd +} + // fakeExecCommand creates a new reference to an exec.Cmd object which has been transformed // to use the supplied parameters as arguments to be submitted to our test runner binary. // It should never be used directly. diff --git a/cli/util/docker.go b/cli/util/docker.go index 404ded8..f4de01d 100644 --- a/cli/util/docker.go +++ b/cli/util/docker.go @@ -11,10 +11,10 @@ import ( // Allow testing code to override the behavior of exec.Command or modify the // resulting exec.Cmd object. -var execCommand = exec.Command +var ExecCommand = exec.Command func GetRawCurrentDockerVersion() string { - output, _ := execCommand("docker", "--version").Output() + output, _ := ExecCommand("docker", "--version").Output() re := regexp.MustCompile("Docker version (.*),") return re.FindAllStringSubmatch(string(output), -1)[0][1] } @@ -25,14 +25,14 @@ func GetCurrentDockerVersion() *version.Version { } func GetDockerClientApiVersion() *version.Version { - output, _ := execCommand("docker", "version", "--format", "{{.Client.APIVersion}}").Output() + output, _ := ExecCommand("docker", "version", "--format", "{{.Client.APIVersion}}").Output() re := regexp.MustCompile("^([\\d|\\.]+)") versionNumber := re.FindAllStringSubmatch(string(output), -1)[0][1] return version.Must(version.NewVersion(versionNumber)) } func GetDockerServerApiVersion(name string) (*version.Version, error) { - output, err := execCommand("docker-machine", "ssh", name, "docker version --format {{.Server.APIVersion}}").Output() + output, err := ExecCommand("docker-machine", "ssh", name, "docker version --format {{.Server.APIVersion}}").Output() if err != nil { return nil, err } @@ -40,7 +40,7 @@ func GetDockerServerApiVersion(name string) (*version.Version, error) { } func GetDockerServerMinApiVersion(name string) (*version.Version, error) { - output, err := execCommand("docker-machine", "ssh", name, "docker version --format {{.Server.MinAPIVersion}}").Output() + output, err := ExecCommand("docker-machine", "ssh", name, "docker version --format {{.Server.MinAPIVersion}}").Output() if err != nil { return nil, err } @@ -49,7 +49,7 @@ func GetDockerServerMinApiVersion(name string) (*version.Version, error) { // Determine the age of the Docker Image and whether the image is older than the designated timestamp. func ImageOlderThan(image string, elapsed_seconds float64) (bool, float64, error) { - output, err := execCommand("docker", "inspect", "--format", "{{.Created}}", image).Output() + output, err := ExecCommand("docker", "inspect", "--format", "{{.Created}}", image).Output() if err != nil { return false, 0, err } diff --git a/cli/util/docker_test.go b/cli/util/docker_test.go index 25def46..5da203d 100644 --- a/cli/util/docker_test.go +++ b/cli/util/docker_test.go @@ -1,9 +1,10 @@ -package util +package util_test import ( "testing" rigtest "github.com/phase2/rig/cli/testing" + "github.com/phase2/rig/cli/util" ) // mock provides mock values to use as lookup responses to functions we will execute in our production code. @@ -19,114 +20,77 @@ var mockSet = rigtest.ExecMockSet{ "docker inspect --format {{.Created}} outrigger/dust": "2017-09-18T21:43:00.565978065Z", } -func TestMain(m *testing.M) { +func init() { rigtest.SetMockByType("success", mockSet) - rigtest.MainTestProcess(m) } // TestGetRawCurrentDockerVersion confirms successful Docker version extraction. func TestGetRawCurrentDockerVersion(t *testing.T) { // In case some other functionality has swapped out this value, we will store // it explicitly rather than assume it is exec.Command. - stashCommand := execCommand - // Re-define execCommand so our runtime code executes using the mocking functionality. - // I thought execCommand would be a private variable in file scope, apparently sharing the package + stashCommand := util.ExecCommand + // Re-define util.ExecCommand so our runtime code executes using the mocking functionality. + // I thought util.ExecCommand would be a private variable in file scope, apparently sharing the package // is enough to access and manipulate it. Or perhaps test functions have special scope rules? - execCommand = rigtest.MockExecCommand + util.ExecCommand = rigtest.MockExecCommand // Put back the original behavior after we are done with this test function. - defer func() { execCommand = stashCommand }() + defer func() { util.ExecCommand = stashCommand }() // Run the code under test. - out := GetRawCurrentDockerVersion() - - // Implement our assertion. - expected := "17.09.0-ce" - if out != expected { - t.Errorf("GetRawCurrentDockerVersion: Expected %q, Actual %q", expected, out) - } + actual := util.GetRawCurrentDockerVersion() + rigtest.Equals(t, "17.09.0-ce", actual) } // TestGetCurrentDockerVersion confirms successful processing of Docker version into version object. // For more thoroughly commented exec wrangling details see TestGetRawCurrentDockerVersion. func TestGetCurrentDockerVersion(t *testing.T) { - stashCommand := execCommand - execCommand = rigtest.MockExecCommand - defer func() { execCommand = stashCommand }() - version, err := GetDockerServerApiVersion("gastropod") - - if err != nil { - t.Errorf("GetDockerServerApiVersion: %v", err) - } - - expected := "1.30.0" - if version.String() != expected { - t.Errorf("GetDockerServerApiVersion: Expected %q, Actual %q", expected, version) - } + stashCommand := util.ExecCommand + util.ExecCommand = rigtest.MockExecCommand + defer func() { util.ExecCommand = stashCommand }() + actual, err := util.GetDockerServerApiVersion("gastropod") + rigtest.Ok(t, err) + rigtest.Equals(t, "1.30.0", actual.String()) } // TestGetDockerServerApiVersion confirms successful Docker client version extraction. // For more thoroughly commented exec wrangling details see TestGetRawCurrentDockerVersion. func TestGetDockerClientApiVersion(t *testing.T) { - stashCommand := execCommand - execCommand = rigtest.MockExecCommand - defer func() { execCommand = stashCommand }() - version := GetDockerClientApiVersion() - - expected := "1.30.0" - if version.String() != expected { - t.Errorf("GetDockerClientApiVersion: Expected %q, Actual %q", expected, version) - } + stashCommand := util.ExecCommand + util.ExecCommand = rigtest.MockExecCommand + defer func() { util.ExecCommand = stashCommand }() + actual := util.GetDockerClientApiVersion() + rigtest.Equals(t, "1.30.0", actual.String()) } // TestGetDockerServerApiVersion confirms successful Docker server version extraction. // For more thoroughly commented exec wrangling details see TestGetRawCurrentDockerVersion. func TestGetDockerServerApiVersion(t *testing.T) { - stashCommand := execCommand - execCommand = rigtest.MockExecCommand - defer func() { execCommand = stashCommand }() - version, err := GetDockerServerApiVersion("gastropod") - - if err != nil { - t.Errorf("GetDockerServerApiVersion: %v", err) - } - - expected := "1.30.0" - if version.String() != expected { - t.Errorf("GetDockerServerApiVersion: Expected %q, Actual %q", expected, version) - } + stashCommand := util.ExecCommand + util.ExecCommand = rigtest.MockExecCommand + defer func() { util.ExecCommand = stashCommand }() + actual, err := util.GetDockerServerApiVersion("gastropod") + rigtest.Ok(t, err) + rigtest.Equals(t, "1.30.0", actual.String()) } // TestGetDockerServerMinApiVersion confirms successful Docker minimum API compatibility version. // For more thoroughly commented exec wrangling details see TestGetRawCurrentDockerVersion. func TestGetDockerServerMinApiVersion(t *testing.T) { - stashCommand := execCommand - execCommand = rigtest.MockExecCommand - defer func() { execCommand = stashCommand }() - version, err := GetDockerServerMinApiVersion("gastropod") - - if err != nil { - t.Errorf("GetDockerServerMinApiVersion: %v", err) - } - - expected := "1.12.0" - if version.String() != expected { - t.Errorf("GetDockerServerMinApiVersion: Expected %q, Actual %q", expected, version) - } + stashCommand := util.ExecCommand + util.ExecCommand = rigtest.MockExecCommand + defer func() { util.ExecCommand = stashCommand }() + actual, err := util.GetDockerServerMinApiVersion("gastropod") + rigtest.Ok(t, err) + rigtest.Equals(t, "1.12.0", actual.String()) } // TestImageOlderThan confirms image age evaluation. // For more thoroughly commented exec wrangling details see TestGetRawCurrentDockerVersion. // @TODO identify how to mock the current time so we can test this more completely. func TestImageOlderThan(t *testing.T) { - stashCommand := execCommand - execCommand = rigtest.MockExecCommand - defer func() { execCommand = stashCommand }() - older, _, err := ImageOlderThan("outrigger/dust", 86400*30) - - if err != nil { - t.Errorf("ImageOlderThan: %v", err) - } - - if !older { - t.Errorf("ImageOlderThan: Image is older than 30 days ago but reporting as newer.") - } + stashCommand := util.ExecCommand + util.ExecCommand = rigtest.MockExecCommand + defer func() { util.ExecCommand = stashCommand }() + older, _, err := util.ImageOlderThan("outrigger/dust", 86400*30) + rigtest.Ok(t, err) + rigtest.Assert(t, older, "Image is older than 30 days ago but reporting as newer.", "howdy") } diff --git a/cli/util/logger_test.go b/cli/util/logger_test.go index c4097ab..24d4ee9 100644 --- a/cli/util/logger_test.go +++ b/cli/util/logger_test.go @@ -1,19 +1,18 @@ -package util +package util_test import ( "testing" + + rigtest "github.com/phase2/rig/cli/testing" + "github.com/phase2/rig/cli/util" ) func TestLoggerInit(t *testing.T) { - LoggerInit(false) - logger := Logger() - if logger.IsVerbose { - t.Error("Logger initialized in non-Verbose mode.") - } + util.LoggerInit(false) + logger := util.Logger() + rigtest.Assert(t, !logger.IsVerbose, "Logger initialized in Verbose mode.") - LoggerInit(true) - logger = Logger() - if !logger.IsVerbose { - t.Error("Logger initialized in Verbose mode.") - } + util.LoggerInit(true) + logger = util.Logger() + rigtest.Assert(t, logger.IsVerbose, "Logger initialized in non-Verbose mode.") } diff --git a/cli/util/shell_exec_test.go b/cli/util/shell_exec_test.go new file mode 100644 index 0000000..e29e4b0 --- /dev/null +++ b/cli/util/shell_exec_test.go @@ -0,0 +1,18 @@ +package util_test + +import ( + "testing" + + rigtest "github.com/phase2/rig/cli/testing" + "github.com/phase2/rig/cli/util" +) + +// TestPassThruCommand confirms we receive the exit code. +// For more thoroughly commented exec wrangling details see docker_test.go::TestGetRawCurrentDockerVersion. +func TestPassthruCommand(t *testing.T) { + actual := util.PassthruCommand(rigtest.SuccessExecCommand("ls")) + rigtest.Equals(t, 0, actual) + + actual = util.PassthruCommand(rigtest.FailExecCommand("ls")) + rigtest.Equals(t, 42, actual) +} diff --git a/cli/util/util_test.go b/cli/util/util_test.go new file mode 100644 index 0000000..33b19fe --- /dev/null +++ b/cli/util/util_test.go @@ -0,0 +1,14 @@ +package util_test + +import ( + "testing" + + rigtest "github.com/phase2/rig/cli/testing" +) + +// Controls the test execution fo the util sub-package. +// Note that if tests were to be run for the entire package cross-package +// duplication of this function would cause it to explode. +func TestMain(m *testing.M) { + rigtest.MainTestProcess(m) +}