From 85f7ed8cfabbc2e737b7176346e65d8d5f02fa0c Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 14 Aug 2017 14:28:08 -0400 Subject: [PATCH 1/4] Add gotestyourself dependency. Signed-off-by: Daniel Nephin --- vendor.conf | 1 + .../gotestyourself/gotestyourself/LICENSE | 202 +++++++++++++ .../gotestyourself/gotestyourself/README.md | 32 ++ .../gotestyourself/golden/golden.go | 71 +++++ .../gotestyourself/icmd/command.go | 277 ++++++++++++++++++ .../gotestyourself/icmd/exitcode.go | 32 ++ 6 files changed, 615 insertions(+) create mode 100644 vendor/github.com/gotestyourself/gotestyourself/LICENSE create mode 100644 vendor/github.com/gotestyourself/gotestyourself/README.md create mode 100644 vendor/github.com/gotestyourself/gotestyourself/golden/golden.go create mode 100644 vendor/github.com/gotestyourself/gotestyourself/icmd/command.go create mode 100644 vendor/github.com/gotestyourself/gotestyourself/icmd/exitcode.go diff --git a/vendor.conf b/vendor.conf index 1e9e72ff43..d27abce96a 100755 --- a/vendor.conf +++ b/vendor.conf @@ -22,6 +22,7 @@ github.com/docker/swarmkit 79381d0840be27f8b3f5c667b348a4467d866eeb github.com/flynn-archive/go-shlex 3f9db97f856818214da2e1057f8ad84803971cff github.com/gogo/protobuf 7efa791bd276fd4db00867cbd982b552627c24cb github.com/golang/protobuf 7a211bcf3bce0e3f1d74f9894916e6f116ae83b4 +github.com/gotestyourself/gotestyourself v1.0.0 github.com/gorilla/context v1.1 github.com/gorilla/mux v1.1 github.com/inconshreveable/mousetrap 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 diff --git a/vendor/github.com/gotestyourself/gotestyourself/LICENSE b/vendor/github.com/gotestyourself/gotestyourself/LICENSE new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/vendor/github.com/gotestyourself/gotestyourself/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/gotestyourself/gotestyourself/README.md b/vendor/github.com/gotestyourself/gotestyourself/README.md new file mode 100644 index 0000000000..2648074900 --- /dev/null +++ b/vendor/github.com/gotestyourself/gotestyourself/README.md @@ -0,0 +1,32 @@ +# Go Test Yourself + +A collection of packages compatible with `go test` to support common testing +patterns. + +[![GoDoc](https://godoc.org/github.com/gotestyourself/gotestyourself?status.svg)](https://godoc.org/github.com/gotestyourself/gotestyourself) +[![CircleCI](https://circleci.com/gh/gotestyourself/gotestyourself/tree/master.svg?style=shield)](https://circleci.com/gh/gotestyourself/gotestyourself/tree/master) +[![Go Reportcard](https://goreportcard.com/badge/github.com/gotestyourself/gotestyourself)](https://goreportcard.com/report/github.com/gotestyourself/gotestyourself) + + +## Packages + +* [fs](http://godoc.org/github.com/gotestyourself/gotestyourself/fs) - + create test files and directories +* [golden](http://godoc.org/github.com/gotestyourself/gotestyourself/golden) - + compare large multi-line strings +* [testsum](http://godoc.org/github.com/gotestyourself/gotestyourself/testsum) - + a program to summarize `go test` output and test failures +* [icmd](http://godoc.org/github.com/gotestyourself/gotestyourself/icmd) - + execute binaries and test the output +* [skip](http://godoc.org/github.com/gotestyourself/gotestyourself/skip) - + skip tests based on conditions + + +## Related + +* [testify/assert](https://godoc.org/github.com/stretchr/testify/assert) and + [testify/require](https://godoc.org/github.com/stretchr/testify/require) - + assertion libraries with common assertions +* [golang/mock](https://github.com/golang/mock) - generate mocks for interfaces +* [testify/suite](https://godoc.org/github.com/stretchr/testify/suite) - + group test into suites to share common setup/teardown logic diff --git a/vendor/github.com/gotestyourself/gotestyourself/golden/golden.go b/vendor/github.com/gotestyourself/gotestyourself/golden/golden.go new file mode 100644 index 0000000000..230ff685cd --- /dev/null +++ b/vendor/github.com/gotestyourself/gotestyourself/golden/golden.go @@ -0,0 +1,71 @@ +/*Package golden provides tools for comparing large mutli-line strings. + +Golden files are files in the ./testdata/ subdirectory of the package under test. +*/ +package golden + +import ( + "flag" + "fmt" + "io/ioutil" + "path/filepath" + + "github.com/pmezard/go-difflib/difflib" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var flagUpdate = flag.Bool("test.update-golden", false, "update golden file") + +// Get returns the golden file content +func Get(t require.TestingT, filename string) []byte { + expected, err := ioutil.ReadFile(Path(filename)) + require.NoError(t, err) + return expected +} + +// Path returns the full path to a golden file +func Path(filename string) string { + return filepath.Join("testdata", filename) +} + +func update(t require.TestingT, filename string, actual []byte) { + if *flagUpdate { + err := ioutil.WriteFile(Path(filename), actual, 0644) + require.NoError(t, err) + } +} + +// Assert compares the actual content to the expected content in the golden file. +// If `--update-golden` is set then the actual content is written to the golden +// file. +// Returns whether the assertion was successful (true) or not (false) +func Assert(t require.TestingT, actual string, filename string, msgAndArgs ...interface{}) bool { + expected := Get(t, filename) + update(t, filename, []byte(actual)) + + if assert.ObjectsAreEqual(expected, []byte(actual)) { + return true + } + + diff, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ + A: difflib.SplitLines(string(expected)), + B: difflib.SplitLines(actual), + FromFile: "Expected", + ToFile: "Actual", + Context: 3, + }) + require.NoError(t, err, msgAndArgs...) + return assert.Fail(t, fmt.Sprintf("Not Equal: \n%s", diff), msgAndArgs...) +} + +// AssertBytes compares the actual result to the expected result in the golden +// file. If `--update-golden` is set then the actual content is written to the +// golden file. +// Returns whether the assertion was successful (true) or not (false) +// nolint: lll +func AssertBytes(t require.TestingT, actual []byte, filename string, msgAndArgs ...interface{}) bool { + expected := Get(t, filename) + update(t, filename, actual) + return assert.Equal(t, expected, actual, msgAndArgs...) +} diff --git a/vendor/github.com/gotestyourself/gotestyourself/icmd/command.go b/vendor/github.com/gotestyourself/gotestyourself/icmd/command.go new file mode 100644 index 0000000000..2470fdc662 --- /dev/null +++ b/vendor/github.com/gotestyourself/gotestyourself/icmd/command.go @@ -0,0 +1,277 @@ +/*Package icmd executes binaries and provides convenient assertions for testing the results. + */ +package icmd + +import ( + "bytes" + "fmt" + "io" + "os/exec" + "path/filepath" + "runtime" + "strings" + "sync" + "time" +) + +type testingT interface { + Fatalf(string, ...interface{}) +} + +const ( + // None is a token to inform Result.Assert that the output should be empty + None string = "" +) + +type lockedBuffer struct { + m sync.RWMutex + buf bytes.Buffer +} + +func (buf *lockedBuffer) Write(b []byte) (int, error) { + buf.m.Lock() + defer buf.m.Unlock() + return buf.buf.Write(b) +} + +func (buf *lockedBuffer) String() string { + buf.m.RLock() + defer buf.m.RUnlock() + return buf.buf.String() +} + +// Result stores the result of running a command +type Result struct { + Cmd *exec.Cmd + ExitCode int + Error error + // Timeout is true if the command was killed because it ran for too long + Timeout bool + outBuffer *lockedBuffer + errBuffer *lockedBuffer +} + +// Assert compares the Result against the Expected struct, and fails the test if +// any of the expectations are not met. +func (r *Result) Assert(t testingT, exp Expected) *Result { + err := r.Compare(exp) + if err == nil { + return r + } + _, file, line, ok := runtime.Caller(1) + if ok { + t.Fatalf("at %s:%d - %s\n", filepath.Base(file), line, err.Error()) + } else { + t.Fatalf("(no file/line info) - %s", err.Error()) + } + return nil +} + +// Compare returns a formatted error with the command, stdout, stderr, exit +// code, and any failed expectations +// nolint: gocyclo +func (r *Result) Compare(exp Expected) error { + errors := []string{} + add := func(format string, args ...interface{}) { + errors = append(errors, fmt.Sprintf(format, args...)) + } + + if exp.ExitCode != r.ExitCode { + add("ExitCode was %d expected %d", r.ExitCode, exp.ExitCode) + } + if exp.Timeout != r.Timeout { + if exp.Timeout { + add("Expected command to timeout") + } else { + add("Expected command to finish, but it hit the timeout") + } + } + if !matchOutput(exp.Out, r.Stdout()) { + add("Expected stdout to contain %q", exp.Out) + } + if !matchOutput(exp.Err, r.Stderr()) { + add("Expected stderr to contain %q", exp.Err) + } + switch { + // If a non-zero exit code is expected there is going to be an error. + // Don't require an error message as well as an exit code because the + // error message is going to be "exit status which is not useful + case exp.Error == "" && exp.ExitCode != 0: + case exp.Error == "" && r.Error != nil: + add("Expected no error") + case exp.Error != "" && r.Error == nil: + add("Expected error to contain %q, but there was no error", exp.Error) + case exp.Error != "" && !strings.Contains(r.Error.Error(), exp.Error): + add("Expected error to contain %q", exp.Error) + } + + if len(errors) == 0 { + return nil + } + return fmt.Errorf("%s\nFailures:\n%s", r, strings.Join(errors, "\n")) +} + +func matchOutput(expected string, actual string) bool { + switch expected { + case None: + return actual == "" + default: + return strings.Contains(actual, expected) + } +} + +func (r *Result) String() string { + var timeout string + if r.Timeout { + timeout = " (timeout)" + } + + return fmt.Sprintf(` +Command: %s +ExitCode: %d%s +Error: %v +Stdout: %v +Stderr: %v +`, + strings.Join(r.Cmd.Args, " "), + r.ExitCode, + timeout, + r.Error, + r.Stdout(), + r.Stderr()) +} + +// Expected is the expected output from a Command. This struct is compared to a +// Result struct by Result.Assert(). +type Expected struct { + ExitCode int + Timeout bool + Error string + Out string + Err string +} + +// Success is the default expected result. A Success result is one with a 0 +// ExitCode. +var Success = Expected{} + +// Stdout returns the stdout of the process as a string +func (r *Result) Stdout() string { + return r.outBuffer.String() +} + +// Stderr returns the stderr of the process as a string +func (r *Result) Stderr() string { + return r.errBuffer.String() +} + +// Combined returns the stdout and stderr combined into a single string +func (r *Result) Combined() string { + return r.outBuffer.String() + r.errBuffer.String() +} + +// SetExitError sets Error and ExitCode based on Error +func (r *Result) SetExitError(err error) { + if err == nil { + return + } + r.Error = err + r.ExitCode = processExitCode(err) +} + +// Cmd contains the arguments and options for a process to run as part of a test +// suite. +type Cmd struct { + Command []string + Timeout time.Duration + Stdin io.Reader + Stdout io.Writer + Dir string + Env []string +} + +// Command create a simple Cmd with the specified command and arguments +func Command(command string, args ...string) Cmd { + return Cmd{Command: append([]string{command}, args...)} +} + +// RunCmd runs a command and returns a Result +func RunCmd(cmd Cmd, cmdOperators ...func(*Cmd)) *Result { + for _, op := range cmdOperators { + op(&cmd) + } + result := StartCmd(cmd) + if result.Error != nil { + return result + } + return WaitOnCmd(cmd.Timeout, result) +} + +// RunCommand parses a command line and runs it, returning a result +func RunCommand(command string, args ...string) *Result { + return RunCmd(Command(command, args...)) +} + +// StartCmd starts a command, but doesn't wait for it to finish +func StartCmd(cmd Cmd) *Result { + result := buildCmd(cmd) + if result.Error != nil { + return result + } + result.SetExitError(result.Cmd.Start()) + return result +} + +func buildCmd(cmd Cmd) *Result { + var execCmd *exec.Cmd + switch len(cmd.Command) { + case 1: + execCmd = exec.Command(cmd.Command[0]) + default: + execCmd = exec.Command(cmd.Command[0], cmd.Command[1:]...) + } + outBuffer := new(lockedBuffer) + errBuffer := new(lockedBuffer) + + execCmd.Stdin = cmd.Stdin + execCmd.Dir = cmd.Dir + execCmd.Env = cmd.Env + if cmd.Stdout != nil { + execCmd.Stdout = io.MultiWriter(outBuffer, cmd.Stdout) + } else { + execCmd.Stdout = outBuffer + } + execCmd.Stderr = errBuffer + return &Result{ + Cmd: execCmd, + outBuffer: outBuffer, + errBuffer: errBuffer, + } +} + +// WaitOnCmd waits for a command to complete. If timeout is non-nil then +// only wait until the timeout. +func WaitOnCmd(timeout time.Duration, result *Result) *Result { + if timeout == time.Duration(0) { + result.SetExitError(result.Cmd.Wait()) + return result + } + + done := make(chan error, 1) + // Wait for command to exit in a goroutine + go func() { + done <- result.Cmd.Wait() + }() + + select { + case <-time.After(timeout): + killErr := result.Cmd.Process.Kill() + if killErr != nil { + fmt.Printf("failed to kill (pid=%d): %v\n", result.Cmd.Process.Pid, killErr) + } + result.Timeout = true + case err := <-done: + result.SetExitError(err) + } + return result +} diff --git a/vendor/github.com/gotestyourself/gotestyourself/icmd/exitcode.go b/vendor/github.com/gotestyourself/gotestyourself/icmd/exitcode.go new file mode 100644 index 0000000000..32272b4bbb --- /dev/null +++ b/vendor/github.com/gotestyourself/gotestyourself/icmd/exitcode.go @@ -0,0 +1,32 @@ +package icmd + +import ( + "os/exec" + "syscall" + + "github.com/pkg/errors" +) + +// GetExitCode returns the ExitStatus of a process from the error returned by +// exec.Run(). If the exit status could not be parsed an error is returned. +func GetExitCode(err error) (int, error) { + if exiterr, ok := err.(*exec.ExitError); ok { + if procExit, ok := exiterr.Sys().(syscall.WaitStatus); ok { + return procExit.ExitStatus(), nil + } + } + return 0, errors.Wrap(err, "failed to get exit code") +} + +func processExitCode(err error) (exitCode int) { + if err == nil { + return 0 + } + exitCode, exiterr := GetExitCode(err) + if exiterr != nil { + // TODO: Fix this so we check the error's text. + // we've failed to retrieve exit code, so we set it to 127 + return 127 + } + return exitCode +} From 26418a12fb80375ec10d8d7564d857560eb33994 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 14 Aug 2017 15:47:06 -0400 Subject: [PATCH 2/4] Add scripts for setting up e2e test environment. Signed-off-by: Daniel Nephin --- Makefile | 8 +-- docker.Makefile | 10 +++- dockerfiles/Dockerfile.test-e2e-env | 15 +++++ e2e/compose-env.yaml | 10 ++++ scripts/test/e2e/load-alpine | 8 +++ scripts/test/e2e/run | 85 +++++++++++++++++++++++++++++ scripts/test/e2e/wait-on-daemon | 9 +++ scripts/test/e2e/wrapper | 33 +++++++++++ scripts/test/watch | 2 +- 9 files changed, 172 insertions(+), 8 deletions(-) create mode 100644 dockerfiles/Dockerfile.test-e2e-env create mode 100644 e2e/compose-env.yaml create mode 100755 scripts/test/e2e/load-alpine create mode 100755 scripts/test/e2e/run create mode 100755 scripts/test/e2e/wait-on-daemon create mode 100755 scripts/test/e2e/wrapper diff --git a/Makefile b/Makefile index ccf36b4665..17ba8e5b2d 100644 --- a/Makefile +++ b/Makefile @@ -10,13 +10,13 @@ _:=$(shell ./scripts/warn-outside-container $(MAKECMDGOALS)) clean: ## remove build artifacts rm -rf ./build/* cli/winresources/rsrc_* ./man/man[1-9] docs/yaml/gen -.PHONY: test -test: ## run go test - ./scripts/test/unit $(shell go list ./... | grep -v '/vendor/') +.PHONY: test-unit +test-unit: ## run unit test + ./scripts/test/unit $(shell go list ./... | grep -vE '/vendor/|/e2e/') .PHONY: test-coverage test-coverage: ## run test coverage - ./scripts/test/unit-with-coverage $(shell go list ./... | grep -v '/vendor/') + ./scripts/test/unit-with-coverage $(shell go list ./... | grep -vE '/vendor/|/e2e/') .PHONY: lint lint: ## run all the lint tools diff --git a/docker.Makefile b/docker.Makefile index b7a34da314..2054c89209 100644 --- a/docker.Makefile +++ b/docker.Makefile @@ -42,9 +42,9 @@ clean: build_docker_image docker run --rm $(ENVVARS) $(MOUNTS) $(DEV_DOCKER_IMAGE_NAME) make clean # run go test -.PHONY: test -test: build_docker_image - docker run --rm $(ENVVARS) $(MOUNTS) $(DEV_DOCKER_IMAGE_NAME) make test +.PHONY: test-unit +test-unit: build_docker_image + docker run --rm $(ENVVARS) $(MOUNTS) $(DEV_DOCKER_IMAGE_NAME) make test-unit # build the CLI for multiple architectures using a container .PHONY: cross @@ -90,3 +90,7 @@ yamldocs: build_docker_image .PHONY: shellcheck shellcheck: build_shell_validate_image docker run -ti --rm $(ENVVARS) $(MOUNTS) $(VALIDATE_IMAGE_NAME) make shellcheck + +.PHONY: test-e2e: +test-e2e: binary + ./scripts/test/e2e/wrapper diff --git a/dockerfiles/Dockerfile.test-e2e-env b/dockerfiles/Dockerfile.test-e2e-env new file mode 100644 index 0000000000..c16b914bdb --- /dev/null +++ b/dockerfiles/Dockerfile.test-e2e-env @@ -0,0 +1,15 @@ +FROM docker/compose:1.15.0 + +RUN apk add -U bash curl + +RUN curl -Ls https://download.docker.com/linux/static/edge/x86_64/docker-17.06.0-ce.tgz | \ + tar -xz docker/docker && \ + mv docker/docker /usr/local/bin/ && \ + rmdir docker +ENV DISABLE_WARN_OUTSIDE_CONTAINER=1 +WORKDIR /work +COPY scripts/test/e2e scripts/test/e2e +COPY e2e/compose-env.yaml e2e/compose-env.yaml + +ENTRYPOINT ["bash", "/work/scripts/test/e2e/run"] +CMD [] diff --git a/e2e/compose-env.yaml b/e2e/compose-env.yaml new file mode 100644 index 0000000000..f16a23c608 --- /dev/null +++ b/e2e/compose-env.yaml @@ -0,0 +1,10 @@ +version: '3.3' + +services: + registry: + image: 'registry:2' + + engine: + image: 'docker:${TEST_ENGINE_VERSION:-edge-dind}' + privileged: true + command: ['--insecure-registry=registry:5000'] diff --git a/scripts/test/e2e/load-alpine b/scripts/test/e2e/load-alpine new file mode 100755 index 0000000000..5b75f0989c --- /dev/null +++ b/scripts/test/e2e/load-alpine @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +src=alpine:3.6 +dest=registry:5000/alpine:3.6 +docker pull $src +docker tag $src $dest +docker push $dest diff --git a/scripts/test/e2e/run b/scripts/test/e2e/run new file mode 100755 index 0000000000..936b5898fd --- /dev/null +++ b/scripts/test/e2e/run @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# Run integration tests against the latest docker-ce dind +set -eu -o pipefail + +function container_ip { + local cid=$1 + local network=$2 + docker inspect \ + -f "{{.NetworkSettings.Networks.${network}.IPAddress}}" "$cid" +} + +function setup { + local project=$1 + COMPOSE_PROJECT_NAME=$1 COMPOSE_FILE=$2 docker-compose up -d >&2 + + local network="${project}_default" + # TODO: only run if inside a container + docker network connect "$network" "$(hostname)" + + engine_ip="$(container_ip "${project}_engine_1" "$network")" + engine_host="tcp://$engine_ip:2375" + ( + export DOCKER_HOST="$engine_host" + timeout -t 200 ./scripts/test/e2e/wait-on-daemon + ./scripts/test/e2e/load-alpine + is_swarm_enabled || docker swarm init + ) >&2 + echo "$engine_host" +} + +function is_swarm_enabled { + docker info 2> /dev/null | grep -q 'Swarm: active' +} + +function cleanup { + COMPOSE_PROJECT_NAME=$1 COMPOSE_FILE=$2 docker-compose down >&2 +} + +function runtests { + local engine_host=$1 + + env -i \ + TEST_DOCKER_HOST="$engine_host" \ + GOPATH="$GOPATH" \ + PATH="$PWD/build/" \ + "$(which go)" test -v ./e2e/... +} + +export unique_id="${E2E_UNIQUE_ID:-cliendtoendsuite}" +compose_env_file=./e2e/compose-env.yaml + +cmd=${1-} + +case "$cmd" in + setup) + setup "$unique_id" "$compose_env_file" + exit + ;; + cleanup) + cleanup "$unique_id" "$compose_env_file" + exit + ;; + test) + engine_host=${2-} + if [[ -z "${engine_host}" ]]; then + echo "missing parameter docker engine host" + echo "Usage: $0 test ENGINE_HOST" + exit 3 + fi + runtests "$engine_host" + ;; + run|"") + engine_host="$(setup "$unique_id" "$compose_env_file")" + testexit=0 + runtests "$engine_host" || testexit=$? + cleanup "$unique_id" "$compose_env_file" + exit $testexit + ;; + *) + echo "Unknown command: $cmd" + echo "Usage: " + echo " $0 [setup | cleanup | test | run] [engine_host]" + exit 1 + ;; +esac diff --git a/scripts/test/e2e/wait-on-daemon b/scripts/test/e2e/wait-on-daemon new file mode 100755 index 0000000000..d1dd5c39f2 --- /dev/null +++ b/scripts/test/e2e/wait-on-daemon @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +echo "Waiting for docker daemon to become available at $DOCKER_HOST" +while ! docker version > /dev/null; do + sleep 0.3 +done + +docker version diff --git a/scripts/test/e2e/wrapper b/scripts/test/e2e/wrapper new file mode 100755 index 0000000000..a3a4f00b67 --- /dev/null +++ b/scripts/test/e2e/wrapper @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Setup, run and teardown e2e test suite in containers. +set -eu -o pipefail + +unique_id="${E2E_UNIQUE_ID:-cliendtoendsuite}" +e2e_env_image=docker-cli-e2e-env:$unique_id +dev_image=docker-cli-dev:$unique_id + +function run_in_env { + local cmd=$1 + docker run -i --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -e E2E_UNIQUE_ID \ + "$e2e_env_image" "$cmd" +} + +docker build \ + -t "$e2e_env_image" \ + -f dockerfiles/Dockerfile.test-e2e-env . + +docker build \ + -t "$dev_image" \ + -f dockerfiles/Dockerfile.dev . + +engine_host=$(run_in_env setup) +testexit=0 +docker run -i --rm \ + -v "$PWD:/go/src/github.com/docker/cli" \ + --network "${unique_id}_default" \ + "$dev_image" \ + ./scripts/test/e2e/run test "$engine_host" || testexit="$?" +run_in_env cleanup +exit "$testexit" diff --git a/scripts/test/watch b/scripts/test/watch index 6c9745aead..264eb7c249 100755 --- a/scripts/test/watch +++ b/scripts/test/watch @@ -1,3 +1,3 @@ #!/bin/sh # shellcheck disable=SC2016 -exec filewatcher -L 6 -x build -x script go test -timeout 10s -v './${dir}' +exec filewatcher -L 6 -x build -x script go test -timeout 30s -v './${dir}' From b5cb5ee4462cd485348aeb9b2f74ecfd35eb2951 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 14 Aug 2017 14:24:11 -0400 Subject: [PATCH 3/4] Add first e2e test Signed-off-by: Daniel Nephin --- cli/command/stack/remove.go | 8 ++ e2e/stack/main_test.go | 26 ++++++ e2e/stack/remove_test.go | 89 +++++++++++++++++++ e2e/stack/testdata/full-stack.yml | 9 ++ .../testdata/stack-remove-success.golden | 3 + 5 files changed, 135 insertions(+) create mode 100644 e2e/stack/main_test.go create mode 100644 e2e/stack/remove_test.go create mode 100644 e2e/stack/testdata/full-stack.yml create mode 100644 e2e/stack/testdata/stack-remove-success.golden diff --git a/cli/command/stack/remove.go b/cli/command/stack/remove.go index d95171aabf..157f71ea12 100644 --- a/cli/command/stack/remove.go +++ b/cli/command/stack/remove.go @@ -2,6 +2,7 @@ package stack import ( "fmt" + "sort" "strings" "github.com/docker/cli/cli" @@ -88,12 +89,19 @@ func runRemove(dockerCli command.Cli, opts removeOptions) error { return nil } +func sortServiceByName(services []swarm.Service) func(i, j int) bool { + return func(i, j int) bool { + return services[i].Spec.Name < services[j].Spec.Name + } +} + func removeServices( ctx context.Context, dockerCli command.Cli, services []swarm.Service, ) bool { var hasError bool + sort.Slice(services, sortServiceByName(services)) for _, service := range services { fmt.Fprintf(dockerCli.Err(), "Removing service %s\n", service.Spec.Name) if err := dockerCli.Client().ServiceRemove(ctx, service.ID); err != nil { diff --git a/e2e/stack/main_test.go b/e2e/stack/main_test.go new file mode 100644 index 0000000000..74081f457b --- /dev/null +++ b/e2e/stack/main_test.go @@ -0,0 +1,26 @@ +package stack + +import ( + "fmt" + "os" + "testing" + + "github.com/pkg/errors" +) + +func TestMain(m *testing.M) { + if err := setupTestEnv(); err != nil { + fmt.Println(err.Error()) + os.Exit(3) + } + os.Exit(m.Run()) +} + +// TODO: move to shared internal package +func setupTestEnv() error { + dockerHost := os.Getenv("TEST_DOCKER_HOST") + if dockerHost == "" { + return errors.New("$TEST_DOCKER_HOST must be set") + } + return os.Setenv("DOCKER_HOST", dockerHost) +} diff --git a/e2e/stack/remove_test.go b/e2e/stack/remove_test.go new file mode 100644 index 0000000000..77d95c793d --- /dev/null +++ b/e2e/stack/remove_test.go @@ -0,0 +1,89 @@ +package stack + +import ( + "fmt" + "strings" + "testing" + "time" + + shlex "github.com/flynn-archive/go-shlex" + "github.com/gotestyourself/gotestyourself/golden" + "github.com/gotestyourself/gotestyourself/icmd" + "github.com/stretchr/testify/require" +) + +func TestRemove(t *testing.T) { + stackname := "test-stack-remove" + deployFullStack(t, stackname) + defer cleanupFullStack(t, stackname) + + result := icmd.RunCmd(shell(t, "docker stack rm %s", stackname)) + + result.Assert(t, icmd.Expected{Out: icmd.None}) + golden.Assert(t, result.Stderr(), "stack-remove-success.golden") +} + +func deployFullStack(t *testing.T, stackname string) { + // TODO: this stack should have full options not minimal options + result := icmd.RunCmd(shell(t, + "docker stack deploy --compose-file=./testdata/full-stack.yml %s", stackname)) + result.Assert(t, icmd.Success) + + waitOn(t, taskCount(stackname, 2), 0) +} + +func cleanupFullStack(t *testing.T, stackname string) { + result := icmd.RunCmd(shell(t, "docker stack rm %s", stackname)) + result.Assert(t, icmd.Success) + waitOn(t, taskCount(stackname, 0), 0) +} + +func taskCount(stackname string, expected int) func() (bool, error) { + return func() (bool, error) { + result := icmd.RunCommand( + "docker", "stack", "ps", "-f=desired-state=running", stackname) + count := lines(result.Stdout()) - 1 + return count == expected, nil + } +} + +func lines(out string) int { + return len(strings.Split(strings.TrimSpace(out), "\n")) +} + +// TODO: move to gotestyourself +func shell(t *testing.T, format string, args ...interface{}) icmd.Cmd { + cmd, err := shlex.Split(fmt.Sprintf(format, args...)) + require.NoError(t, err) + return icmd.Cmd{Command: cmd} +} + +// TODO: move to gotestyourself +func waitOn(t *testing.T, check func() (bool, error), timeout time.Duration) { + if timeout == time.Duration(0) { + timeout = defaultTimeout() + } + + after := time.After(timeout) + for { + select { + case <-after: + // TODO: include check function name in error message + t.Fatalf("timeout hit after %s", timeout) + default: + // TODO: maybe return a failure message as well? + done, err := check() + if done { + return + } + if err != nil { + t.Fatal(err.Error()) + } + } + } +} + +func defaultTimeout() time.Duration { + // TODO: support override from environment variable + return 10 * time.Second +} diff --git a/e2e/stack/testdata/full-stack.yml b/e2e/stack/testdata/full-stack.yml new file mode 100644 index 0000000000..8c4d06f854 --- /dev/null +++ b/e2e/stack/testdata/full-stack.yml @@ -0,0 +1,9 @@ +version: '3.3' + +services: + one: + image: registry:5000/alpine:3.6 + command: top + two: + image: registry:5000/alpine:3.6 + command: top diff --git a/e2e/stack/testdata/stack-remove-success.golden b/e2e/stack/testdata/stack-remove-success.golden new file mode 100644 index 0000000000..f41a891702 --- /dev/null +++ b/e2e/stack/testdata/stack-remove-success.golden @@ -0,0 +1,3 @@ +Removing service test-stack-remove_one +Removing service test-stack-remove_two +Removing network test-stack-remove_default From 63d76065bb34cdf39804ee185be49b77667ba490 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 15 Aug 2017 16:59:43 -0400 Subject: [PATCH 4/4] Add a Jenkinsfile Signed-off-by: Daniel Nephin --- Jenkinsfile | 12 ++++++++++++ docker.Makefile | 10 +++++----- dockerfiles/Dockerfile.test-e2e-env | 6 ++++-- e2e/compose-env.yaml | 2 +- 4 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 Jenkinsfile diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000000..c5fd505597 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,12 @@ +wrappedNode(label: 'linux && x86_64', cleanWorkspace: true) { + timeout(time: 60, unit: 'MINUTES') { + stage "Git Checkout" + checkout scm + + stage "Run end-to-end test suite" + sh "docker version" + sh "E2E_UNIQUE_ID=clie2e${BUILD_NUMBER} \ + IMAGE_TAG=clie2e${BUILD_NUMBER} \ + make -f docker.Makefile test-e2e" + } +} diff --git a/docker.Makefile b/docker.Makefile index 2054c89209..097ad49399 100644 --- a/docker.Makefile +++ b/docker.Makefile @@ -4,10 +4,10 @@ # Makefile for developing using Docker # -DEV_DOCKER_IMAGE_NAME = docker-cli-dev -LINTER_IMAGE_NAME = docker-cli-lint -CROSS_IMAGE_NAME = docker-cli-cross -VALIDATE_IMAGE_NAME = docker-cli-shell-validate +DEV_DOCKER_IMAGE_NAME = docker-cli-dev$(IMAGE_TAG) +LINTER_IMAGE_NAME = docker-cli-lint$(IMAGE_TAG) +CROSS_IMAGE_NAME = docker-cli-cross$(IMAGE_TAG) +VALIDATE_IMAGE_NAME = docker-cli-shell-validate$(IMAGE_TAG) MOUNTS = -v "$(CURDIR)":/go/src/github.com/docker/cli VERSION = $(shell cat VERSION) ENVVARS = -e VERSION=$(VERSION) -e GITCOMMIT @@ -91,6 +91,6 @@ yamldocs: build_docker_image shellcheck: build_shell_validate_image docker run -ti --rm $(ENVVARS) $(MOUNTS) $(VALIDATE_IMAGE_NAME) make shellcheck -.PHONY: test-e2e: +.PHONY: test-e2e test-e2e: binary ./scripts/test/e2e/wrapper diff --git a/dockerfiles/Dockerfile.test-e2e-env b/dockerfiles/Dockerfile.test-e2e-env index c16b914bdb..3c672f4e8c 100644 --- a/dockerfiles/Dockerfile.test-e2e-env +++ b/dockerfiles/Dockerfile.test-e2e-env @@ -2,7 +2,10 @@ FROM docker/compose:1.15.0 RUN apk add -U bash curl -RUN curl -Ls https://download.docker.com/linux/static/edge/x86_64/docker-17.06.0-ce.tgz | \ +ARG DOCKER_CHANNEL=edge +ARG DOCKER_VERSION=17.06.0-ce +RUN export URL=https://download.docker.com/linux/static; \ + curl -Ls $URL/$DOCKER_CHANNEL/x86_64/docker-$DOCKER_VERSION.tgz | \ tar -xz docker/docker && \ mv docker/docker /usr/local/bin/ && \ rmdir docker @@ -12,4 +15,3 @@ COPY scripts/test/e2e scripts/test/e2e COPY e2e/compose-env.yaml e2e/compose-env.yaml ENTRYPOINT ["bash", "/work/scripts/test/e2e/run"] -CMD [] diff --git a/e2e/compose-env.yaml b/e2e/compose-env.yaml index f16a23c608..afc95e3af0 100644 --- a/e2e/compose-env.yaml +++ b/e2e/compose-env.yaml @@ -1,4 +1,4 @@ -version: '3.3' +version: '2.1' services: registry: