Skip to content

Commit

Permalink
First working version of library
Browse files Browse the repository at this point in the history
Signed-off-by: Mike Seplowitz <[email protected]>
  • Loading branch information
mikesep committed Apr 9, 2019
1 parent 1972b01 commit 13f9ae3
Show file tree
Hide file tree
Showing 16 changed files with 974 additions and 0 deletions.
49 changes: 49 additions & 0 deletions README.markdown
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.

&#x26A0; **_Stability warning: API might change_** &#x26A0;

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
10 changes: 10 additions & 0 deletions depsfortestdata_test.go
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"
)
254 changes: 254 additions & 0 deletions dockercompose.go
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)
}
71 changes: 71 additions & 0 deletions dockercompose_test.go
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)
}
}
Loading

0 comments on commit 13f9ae3

Please sign in to comment.