Skip to content

Initial test coverage in util package #97

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ script:
- "scripts/test-go-fmt.sh"
- "gometalinter --vendor --deadline=60s --config=gometalinter.json ./..."
- "go run cmd/main.go"
- "go test github.com/phase2/rig/util"

notifications:
flowdock:
Expand Down
38 changes: 38 additions & 0 deletions cli/testing/assert.go
Original file line number Diff line number Diff line change
@@ -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()
}
}
122 changes: 122 additions & 0 deletions cli/testing/testing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// 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 "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.
// 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 execution
// into something controlled by the test runner, then adds an environment variable to
// 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 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.
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.
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
}
96 changes: 96 additions & 0 deletions cli/util/docker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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.
// 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 init() {
rigtest.SetMockByType("success", mockSet)
}

// 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 := 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?
util.ExecCommand = rigtest.MockExecCommand
// Put back the original behavior after we are done with this test function.
defer func() { util.ExecCommand = stashCommand }()
// Run the code under test.
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 := 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 := 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 := 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 := 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 := 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")
}
18 changes: 18 additions & 0 deletions cli/util/logger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package util_test

import (
"testing"

rigtest "github.com/phase2/rig/cli/testing"
"github.com/phase2/rig/cli/util"
)

func TestLoggerInit(t *testing.T) {
util.LoggerInit(false)
logger := util.Logger()
rigtest.Assert(t, !logger.IsVerbose, "Logger initialized in Verbose mode.")

util.LoggerInit(true)
logger = util.Logger()
rigtest.Assert(t, logger.IsVerbose, "Logger initialized in non-Verbose mode.")
}
18 changes: 18 additions & 0 deletions cli/util/shell_exec_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
14 changes: 14 additions & 0 deletions cli/util/util_test.go
Original file line number Diff line number Diff line change
@@ -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)
}