-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Mike Seplowitz <[email protected]>
- Loading branch information
Showing
16 changed files
with
974 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# docket | ||
|
||
Docket is a library that helps you use | ||
[`docker-compose`][docker-compose-overview] to set up an environment in which | ||
you can run your tests. | ||
|
||
⚠ **_Stability warning: API might change_** ⚠ | ||
|
||
This pre-1.0.0 API is subject to change as we make improvements. | ||
|
||
## Overview | ||
|
||
Docket helps you run a test or test suite inside a `docker-compose` app. If | ||
requested, docket will run bring up a `docker-compose` app, run the test suite, | ||
and optionally shut down the `docker-compose` app. If you don't activate docket, | ||
the test will run on its own as if you weren't using docket at all. | ||
|
||
Docket is compatible with the standard [`testing` package][testing-godoc] as | ||
well as [`testify/suite`][testify-suite-readme]. | ||
|
||
## Examples | ||
|
||
For examples, see the [testdata directory](testdata). | ||
|
||
## Running tests with docket | ||
|
||
In order to be unobtrusive, docket takes its arguments as environment variables. | ||
|
||
For quick help, run `go test -help-docket`, and you'll see this: | ||
|
||
``` | ||
Help for using docket: | ||
GO_DOCKET_CONFIG | ||
To use docket, set this to the name of the config to use. | ||
Optional environment variables: | ||
GO_DOCKET_DOWN (default off) | ||
If non-empty, docket will run 'docker-compose down' after each suite. | ||
GO_DOCKET_PULL (default off) | ||
If non-empty, docket will run 'docker-compose pull' before each suite. | ||
``` | ||
|
||
[docker-compose-overview]: https://docs.docker.com/compose/overview/ | ||
[testing-godoc]: https://godoc.org/testing | ||
[testify-suite-readme]: | ||
https://github.com/stretchr/testify/blob/master/README.md#suite-package |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package docket | ||
|
||
/* | ||
This file lists dependencies of the tests in testdata to make it easier to use | ||
`go get` or `dep` to pull down those dependencies. | ||
*/ | ||
|
||
import ( | ||
_ "gopkg.in/redis.v5" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,254 @@ | ||
package docket | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"flag" | ||
"fmt" | ||
"os" | ||
"os/exec" | ||
"path/filepath" | ||
"regexp" | ||
"strconv" | ||
"strings" | ||
"testing" | ||
) | ||
|
||
func dockerComposeUp(ctx context.Context, config Config) error { | ||
args := append(composeFileArgs(config), "up", "-d") | ||
|
||
cmd := exec.CommandContext(ctx, "docker-compose", args...) | ||
|
||
cmd.Env = append( | ||
os.Environ(), | ||
fmt.Sprintf("GOPATH=%s", determineGOPATH(ctx)), | ||
) | ||
|
||
cmd.Stdout = os.Stderr | ||
cmd.Stderr = os.Stderr | ||
|
||
trace("up %v\n", cmd.Args) | ||
defer trace("up finished\n") | ||
|
||
return cmd.Run() | ||
} | ||
|
||
func dockerComposeDown(ctx context.Context, config Config) error { | ||
args := append(composeFileArgs(config), "down") | ||
|
||
cmd := exec.CommandContext(ctx, "docker-compose", args...) | ||
|
||
cmd.Env = append( | ||
os.Environ(), | ||
fmt.Sprintf("GOPATH=%s", determineGOPATH(ctx)), | ||
) | ||
|
||
cmd.Stdout = os.Stderr | ||
cmd.Stderr = os.Stderr | ||
|
||
trace("down %v\n", cmd.Args) | ||
defer trace("down finished\n") | ||
|
||
return cmd.Run() | ||
} | ||
|
||
func dockerComposeExecGoTest(ctx context.Context, config Config, testName string) error { | ||
var testRunArg string | ||
if f := flag.Lookup("test.run"); f != nil { | ||
testRunArg = f.Value.String() | ||
} | ||
|
||
currentDir, err := os.Getwd() | ||
if err != nil { | ||
return fmt.Errorf("failed to get working directory: %v", err) | ||
} | ||
|
||
testPackage, err := findPackageNameFromCurrentDirAndGOPATH(currentDir, determineGOPATH(ctx)) | ||
if err != nil { | ||
return fmt.Errorf("failed to find package name: %v", err) | ||
} | ||
|
||
args := append( | ||
composeFileArgs(config), | ||
"exec", | ||
"-T", // disable pseudo-tty allocation | ||
config.GoTestExec.Service, | ||
"go", "test", | ||
testPackage, | ||
"-run", makeRunArgForTest(testName, testRunArg)) | ||
|
||
if len(config.GoTestExec.BuildTags) > 0 { | ||
args = append(args, "-tags", strings.Join(config.GoTestExec.BuildTags, " ")) | ||
} | ||
|
||
if testing.Verbose() { | ||
args = append(args, "-v") | ||
} | ||
|
||
cmd := exec.CommandContext(ctx, "docker-compose", args...) | ||
|
||
cmd.Env = append( | ||
os.Environ(), | ||
fmt.Sprintf("GOPATH=%s", determineGOPATH(ctx)), | ||
) | ||
|
||
cmd.Stdout = os.Stdout | ||
cmd.Stderr = os.Stderr | ||
|
||
trace("exec %v\n", cmd.Args) | ||
defer trace("exec finished\n") | ||
|
||
if err := cmd.Run(); err != nil { | ||
return fmt.Errorf("failed to exec go test: %v", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func dockerComposePort(ctx context.Context, config Config, service string, port int) (int, error) { | ||
args := append( | ||
composeFileArgs(config), | ||
"port", | ||
service, | ||
strconv.Itoa(port), | ||
) | ||
|
||
cmd := exec.CommandContext(ctx, "docker-compose", args...) | ||
|
||
cmd.Env = append( | ||
os.Environ(), | ||
fmt.Sprintf("GOPATH=%s", determineGOPATH(ctx)), | ||
) | ||
|
||
trace("port %v\n", cmd.Args) | ||
|
||
out, err := cmd.CombinedOutput() | ||
if err != nil { | ||
return 0, fmt.Errorf("port error: err=%v out=%v", err, out) | ||
} | ||
|
||
re := regexp.MustCompile(":[[:digit:]]+$") | ||
match := re.Find(bytes.TrimSpace(out)) | ||
if len(match) == 0 { | ||
return 0, fmt.Errorf("could not find port number in output: %s", out) | ||
} | ||
|
||
return strconv.Atoi(string(match[1:])) // drop the leading colon | ||
} | ||
|
||
func dockerComposeConfig(ctx context.Context, config Config) ([]byte, error) { | ||
args := append(composeFileArgs(config), "config") | ||
|
||
cmd := exec.CommandContext(ctx, "docker-compose", args...) | ||
|
||
cmd.Env = append( | ||
os.Environ(), | ||
fmt.Sprintf("GOPATH=%s", determineGOPATH(ctx)), | ||
) | ||
|
||
trace("config %v\n", cmd.Args) | ||
|
||
out, err := cmd.CombinedOutput() | ||
if err != nil { | ||
return nil, fmt.Errorf("error getting config: err=%v out=%v", err, out) | ||
} | ||
|
||
return out, nil | ||
} | ||
|
||
func dockerComposePull(ctx context.Context, config Config) error { | ||
args := append(composeFileArgs(config), "pull") | ||
|
||
cmd := exec.CommandContext(ctx, "docker-compose", args...) | ||
|
||
cmd.Env = append( | ||
os.Environ(), | ||
fmt.Sprintf("GOPATH=%s", determineGOPATH(ctx)), | ||
) | ||
|
||
cmd.Stdout = os.Stdout | ||
cmd.Stderr = os.Stderr | ||
|
||
trace("pull %v\n", cmd.Args) | ||
defer trace("pull finished\n") | ||
|
||
return cmd.Run() | ||
} | ||
|
||
//---------------------------------------------------------- | ||
|
||
func determineGOPATH(ctx context.Context) string { | ||
// Is this weird? | ||
cmd := exec.CommandContext(ctx, "go", "env", "GOPATH") | ||
out, err := cmd.CombinedOutput() | ||
if err != nil { | ||
panic(fmt.Sprintf("err=%v, out=%s", err, out)) | ||
} | ||
return strings.TrimSpace(string(out)) | ||
} | ||
|
||
func composeFileArgs(config Config) []string { | ||
args := make([]string, 0, len(config.ComposeFiles)*2) | ||
|
||
for _, file := range config.ComposeFiles { | ||
args = append(args, "-f", file) | ||
} | ||
|
||
return args | ||
} | ||
|
||
// testName cannot be empty. runArg should be empty if no -run arg was used. | ||
func makeRunArgForTest(testName, runArg string) string { | ||
/* | ||
-run regexp | ||
Run only those tests and examples matching the regular expression. | ||
For tests, the regular expression is split by unbracketed slash (/) | ||
characters into a sequence of regular expressions, and each part | ||
of a test's identifier must match the corresponding element in | ||
the sequence, if any. Note that possible parents of matches are | ||
run too, so that -run=X/Y matches and runs and reports the result | ||
of all tests matching X, even those without sub-tests matching Y, | ||
because it must run them to look for those sub-tests. | ||
When we run `go test` inside a docker container, we want to re-run this specific test, | ||
so we use an anchored regexp to exactly match the test and include any other subtest criteria. | ||
*/ | ||
|
||
if testName == "" { | ||
panic("testName was empty") | ||
} | ||
|
||
testParts := strings.Split(testName, "/") | ||
for i := range testParts { | ||
testParts[i] = fmt.Sprintf("^%s$", testParts[i]) | ||
} | ||
|
||
var runParts []string | ||
if runArg != "" { | ||
runParts = strings.Split(runArg, "/") | ||
} | ||
|
||
if len(runParts) > len(testParts) { | ||
return strings.Join(append(testParts, runParts[len(testParts):]...), "/") | ||
} else { | ||
return strings.Join(testParts, "/") | ||
} | ||
} | ||
|
||
func findPackageNameFromCurrentDirAndGOPATH(currentDir, gopath string) (string, error) { | ||
for _, gp := range filepath.SplitList(gopath) { | ||
pathUnderGOPATH, err := filepath.Rel(gp, currentDir) | ||
if err != nil { | ||
continue | ||
} | ||
|
||
srcPrefix := fmt.Sprintf("src%c", filepath.Separator) | ||
if !strings.HasPrefix(pathUnderGOPATH, srcPrefix) { | ||
continue | ||
} | ||
|
||
return pathUnderGOPATH[len(srcPrefix):], nil | ||
} | ||
|
||
return "", fmt.Errorf("could not find package name. currentDir=%q GOPATH=%q", currentDir, gopath) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
package docket | ||
|
||
import ( | ||
"path/filepath" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/suite" | ||
) | ||
|
||
func TestDockerCompose(t *testing.T) { | ||
var s DockerComposeSuite | ||
suite.Run(t, &s) | ||
} | ||
|
||
type DockerComposeSuite struct { | ||
suite.Suite | ||
} | ||
|
||
func (s *DockerComposeSuite) TestMakeRunArgForTest() { | ||
cases := []struct { | ||
TestName, RunArg, Result string | ||
}{ | ||
{"top", "", "^top$"}, | ||
{"top", "p", "^top$"}, | ||
{"top", "p/sub", "^top$/sub"}, | ||
{"top/sub", "", "^top$/^sub$"}, | ||
{"top", "/sub", "^top$/sub"}, | ||
} | ||
|
||
for _, c := range cases { | ||
s.Equal(c.Result, makeRunArgForTest(c.TestName, c.RunArg)) | ||
} | ||
} | ||
|
||
func (s *DockerComposeSuite) TestFindPackageNameFromCurrentDirAndGOPATH() { | ||
goodCases := []struct { | ||
CurDir, GOPATH, Result string | ||
}{ | ||
{ | ||
CurDir: filepath.FromSlash("/go/src/package"), | ||
GOPATH: filepath.FromSlash("/go"), | ||
Result: "package", | ||
}, | ||
{ | ||
CurDir: filepath.FromSlash("/go/src/package"), | ||
GOPATH: strings.Join( | ||
[]string{filepath.FromSlash("/another"), filepath.FromSlash("/go")}, | ||
string(filepath.ListSeparator)), | ||
Result: "package", | ||
}, | ||
} | ||
|
||
for _, c := range goodCases { | ||
pkg, err := findPackageNameFromCurrentDirAndGOPATH(c.CurDir, c.GOPATH) | ||
s.Equal(c.Result, pkg, "case: %v", c) | ||
s.NoError(err, "case: %v", c) | ||
} | ||
|
||
badCases := []struct { | ||
CurDir, GOPATH string | ||
}{ | ||
{"/unrelated/path", "/go"}, | ||
{"/go/src/package", "/mismatched:/gopaths"}, | ||
} | ||
|
||
for _, c := range badCases { | ||
_, err := findPackageNameFromCurrentDirAndGOPATH(c.CurDir, c.GOPATH) | ||
s.Error(err, "case: %v", c) | ||
} | ||
} |
Oops, something went wrong.