From 8b3f0563861ef78a5866031f1e5c714a983faa77 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 28 Jul 2020 01:03:42 -0500 Subject: [PATCH 01/19] Initial setup for integration tests --- ci/integration/integration_test.go | 28 ++++ ci/tcli/tcli.go | 212 +++++++++++++++++++++++++++++ go.mod | 1 + 3 files changed, 241 insertions(+) create mode 100644 ci/integration/integration_test.go create mode 100644 ci/tcli/tcli.go diff --git a/ci/integration/integration_test.go b/ci/integration/integration_test.go new file mode 100644 index 00000000..188fdccb --- /dev/null +++ b/ci/integration/integration_test.go @@ -0,0 +1,28 @@ +package integration + +import ( + "context" + "testing" + "time" + + "cdr.dev/coder-cli/ci/tcli" +) + +func TestTCli(t *testing.T) { + ctx := context.Background() + + container := tcli.NewRunContainer(ctx, "", "test-container") + + container.Run(ctx, "echo testing").Assert(t, + tcli.Success(), + tcli.StderrEmpty(), + tcli.StdoutMatches("esting"), + ) + + container.Run(ctx, "sleep 1.5 && echo 1>&2 stderr-message").Assert(t, + tcli.Success(), + tcli.StdoutEmpty(), + tcli.StderrMatches("message"), + tcli.DurationGreaterThan(time.Second), + ) +} diff --git a/ci/tcli/tcli.go b/ci/tcli/tcli.go new file mode 100644 index 00000000..f03a02c8 --- /dev/null +++ b/ci/tcli/tcli.go @@ -0,0 +1,212 @@ +package tcli + +import ( + "bytes" + "context" + "fmt" + "os/exec" + "regexp" + "testing" + "time" + + "cdr.dev/slog/sloggers/slogtest/assert" + "golang.org/x/xerrors" +) + +type RunContainer struct { +} + +func NewRunContainer(ctx context.Context, image, name string) *RunContainer { + //exec.CommandContext(ctx, "docker", "start") + // TODO: startup docker container + return &RunContainer{} +} + +func (r RunContainer) Teardown() error { + // TODO: teardown run environment + return nil +} + +type Assertable struct { + cmd string + ctx context.Context +} + +func (*RunContainer) Run(ctx context.Context, cmd string) *Assertable { + return &Assertable{ + cmd: cmd, + ctx: ctx, + } +} + +func (a Assertable) Assert(t *testing.T, option ...Assertion) { + var cmdResult CommandResult + + cmd := exec.CommandContext(a.ctx, "sh", "-c", a.cmd) + var ( + stdout bytes.Buffer + stderr bytes.Buffer + ) + + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + start := time.Now() + err := cmd.Run() + cmdResult.Duration = time.Since(start) + + if exitErr, ok := err.(*exec.ExitError); ok { + cmdResult.ExitCode = exitErr.ExitCode() + } else if err != nil { + cmdResult.ExitCode = -1 + } else { + cmdResult.ExitCode = 0 + } + + cmdResult.Stdout = stdout.Bytes() + cmdResult.Stderr = stderr.Bytes() + + for ix, o := range option { + name := fmt.Sprintf("assertion_#%v", ix) + if named, ok := o.(Named); ok { + name = named.Name() + } + t.Run(name, func(t *testing.T) { + err := o.Valid(cmdResult) + assert.Success(t, name, err) + }) + } +} + +type Assertion interface { + Valid(r CommandResult) error +} + +type Named interface { + Name() string +} + +type CommandResult struct { + Stdout, Stderr []byte + ExitCode int + Duration time.Duration +} + +type simpleFuncAssert struct { + valid func(r CommandResult) error + name string +} + +func (s simpleFuncAssert) Valid(r CommandResult) error { + return s.valid(r) +} + +func (s simpleFuncAssert) Name() string { + return s.name +} + +func Success() Assertion { + return ExitCodeIs(0) +} + +func ExitCodeIs(code int) Assertion { + return simpleFuncAssert{ + valid: func(r CommandResult) error { + if r.ExitCode != code { + return xerrors.Errorf("exit code of %s expected, got %v", code, r.ExitCode) + } + return nil + }, + name: fmt.Sprintf("exitcode"), + } +} + +func StdoutEmpty() Assertion { + return simpleFuncAssert{ + valid: func(r CommandResult) error { + return empty("stdout", r.Stdout) + }, + name: fmt.Sprintf("stdout-empty"), + } +} + +func StderrEmpty() Assertion { + return simpleFuncAssert{ + valid: func(r CommandResult) error { + return empty("stderr", r.Stderr) + }, + name: fmt.Sprintf("stderr-empty"), + } +} + +func StdoutMatches(pattern string) Assertion { + return simpleFuncAssert{ + valid: func(r CommandResult) error { + return matches("stdout", pattern, r.Stdout) + }, + name: fmt.Sprintf("stdout-matches"), + } +} + +func StderrMatches(pattern string) Assertion { + return simpleFuncAssert{ + valid: func(r CommandResult) error { + return matches("stderr", pattern, r.Stderr) + }, + name: fmt.Sprintf("stderr-matches"), + } +} + +func CombinedMatches(pattern string) Assertion { + return simpleFuncAssert{ + valid: func(r CommandResult) error { + //stdoutValid := StdoutMatches(pattern).Valid(r) + //stderrValid := StderrMatches(pattern).Valid(r) + // TODO: combine errors + return nil + }, + name: fmt.Sprintf("combined-matches"), + } +} + +func matches(name, pattern string, target []byte) error { + ok, err := regexp.Match(pattern, target) + if err != nil { + return xerrors.Errorf("failed to attempt regexp match: %w", err) + } + if !ok { + return xerrors.Errorf("expected to find pattern (%s) in %s, no match found", pattern, name) + } + return nil +} + +func empty(name string, a []byte) error { + if len(a) > 0 { + return xerrors.Errorf("expected %s to be empty, got (%s)", name, string(a)) + } + return nil +} + +func DurationLessThan(dur time.Duration) Assertion { + return simpleFuncAssert{ + valid: func(r CommandResult) error { + if r.Duration > dur { + return xerrors.Errorf("expected duration less than %s, took %s", dur.String(), r.Duration.String()) + } + return nil + }, + name: fmt.Sprintf("duration-lessthan"), + } +} + +func DurationGreaterThan(dur time.Duration) Assertion { + return simpleFuncAssert{ + valid: func(r CommandResult) error { + if r.Duration < dur { + return xerrors.Errorf("expected duration greater than %s, took %s", dur.String(), r.Duration.String()) + } + return nil + }, + name: fmt.Sprintf("duration-greaterthan"), + } +} diff --git a/go.mod b/go.mod index 7d8feec2..4f496165 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module cdr.dev/coder-cli go 1.14 require ( + cdr.dev/slog v1.3.0 cdr.dev/wsep v0.0.0-20200728013649-82316a09813f github.com/fatih/color v1.9.0 // indirect github.com/gorilla/websocket v1.4.1 From 8e8b215e1a5949c93611632749ad8f534d4bc0e1 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 28 Jul 2020 01:31:23 -0500 Subject: [PATCH 02/19] Execute commands in container --- ci/integration/integration_test.go | 5 ++- ci/tcli/tcli.go | 58 +++++++++++++++++++++++------- 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/ci/integration/integration_test.go b/ci/integration/integration_test.go index 188fdccb..5ecdeb7e 100644 --- a/ci/integration/integration_test.go +++ b/ci/integration/integration_test.go @@ -6,12 +6,15 @@ import ( "time" "cdr.dev/coder-cli/ci/tcli" + "cdr.dev/slog/sloggers/slogtest/assert" ) func TestTCli(t *testing.T) { ctx := context.Background() - container := tcli.NewRunContainer(ctx, "", "test-container") + container, err := tcli.NewRunContainer(ctx, "ubuntu:latest", "test-container") + assert.Success(t, "new run container", err) + defer container.Close() container.Run(ctx, "echo testing").Assert(t, tcli.Success(), diff --git a/ci/tcli/tcli.go b/ci/tcli/tcli.go index f03a02c8..80fa6383 100644 --- a/ci/tcli/tcli.go +++ b/ci/tcli/tcli.go @@ -6,6 +6,7 @@ import ( "fmt" "os/exec" "regexp" + "strings" "testing" "time" @@ -14,35 +15,68 @@ import ( ) type RunContainer struct { + name string + ctx context.Context } -func NewRunContainer(ctx context.Context, image, name string) *RunContainer { - //exec.CommandContext(ctx, "docker", "start") - // TODO: startup docker container - return &RunContainer{} +func NewRunContainer(ctx context.Context, image, name string) (*RunContainer, error) { + cmd := exec.CommandContext(ctx, + "docker", "run", + "--name", name, + "-it", "-d", + image, + ) + + out, err := cmd.CombinedOutput() + if err != nil { + return nil, xerrors.Errorf( + "failed to start testing container %q, (%s): %w", + name, string(out), err) + } + + return &RunContainer{ + name: name, + ctx: ctx, + }, nil } -func (r RunContainer) Teardown() error { - // TODO: teardown run environment +func (r *RunContainer) Close() error { + cmd := exec.CommandContext(r.ctx, + "sh", "-c", strings.Join([]string{ + "docker", "kill", r.name, "&&", + "docker", "rm", r.name, + }, " ")) + + out, err := cmd.CombinedOutput() + if err != nil { + return xerrors.Errorf( + "failed to stop testing container %q, (%s): %w", + r.name, string(out), err) + } return nil } type Assertable struct { - cmd string - ctx context.Context + cmd string + ctx context.Context + container *RunContainer } -func (*RunContainer) Run(ctx context.Context, cmd string) *Assertable { +func (r *RunContainer) Run(ctx context.Context, cmd string) *Assertable { return &Assertable{ - cmd: cmd, - ctx: ctx, + cmd: cmd, + ctx: ctx, + container: r, } } func (a Assertable) Assert(t *testing.T, option ...Assertion) { var cmdResult CommandResult - cmd := exec.CommandContext(a.ctx, "sh", "-c", a.cmd) + cmd := exec.CommandContext(a.ctx, + "docker", "exec", a.container.name, + "sh", "-c", a.cmd, + ) var ( stdout bytes.Buffer stderr bytes.Buffer From 2932fe9188ea0ca1ba28816792ef0b31e3928089 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 28 Jul 2020 09:38:20 -0500 Subject: [PATCH 03/19] Add RunCmd manual command --- ci/integration/integration_test.go | 17 +++++- ci/tcli/tcli.go | 84 +++++++++++++++++++++++------- 2 files changed, 81 insertions(+), 20 deletions(-) diff --git a/ci/integration/integration_test.go b/ci/integration/integration_test.go index 5ecdeb7e..7b8a90e5 100644 --- a/ci/integration/integration_test.go +++ b/ci/integration/integration_test.go @@ -2,6 +2,8 @@ package integration import ( "context" + "os/exec" + "strings" "testing" "time" @@ -12,7 +14,11 @@ import ( func TestTCli(t *testing.T) { ctx := context.Background() - container, err := tcli.NewRunContainer(ctx, "ubuntu:latest", "test-container") + container, err := tcli.NewRunContainer(ctx, &tcli.ContainerConfig{ + Image: "ubuntu:latest", + Name: "test-container", + }) + assert.Success(t, "new run container", err) defer container.Close() @@ -28,4 +34,13 @@ func TestTCli(t *testing.T) { tcli.StderrMatches("message"), tcli.DurationGreaterThan(time.Second), ) + + cmd := exec.CommandContext(ctx, "cat") + cmd.Stdin = strings.NewReader("testing") + + container.RunCmd(cmd).Assert(t, + tcli.Success(), + tcli.StderrEmpty(), + tcli.StdoutMatches("testing"), + ) } diff --git a/ci/tcli/tcli.go b/ci/tcli/tcli.go index 80fa6383..40d7ac6d 100644 --- a/ci/tcli/tcli.go +++ b/ci/tcli/tcli.go @@ -19,23 +19,51 @@ type RunContainer struct { ctx context.Context } -func NewRunContainer(ctx context.Context, image, name string) (*RunContainer, error) { - cmd := exec.CommandContext(ctx, - "docker", "run", - "--name", name, +type ContainerConfig struct { + Name string + Image string + Mounts map[string]string +} + +func mountArgs(m map[string]string) (args []string) { + for src, dest := range m { + args = append(args, "--mount", fmt.Sprintf("source=%s,target=%s", src, dest)) + } + return args +} + +func preflightChecks() error { + _, err := exec.LookPath("docker") + if err != nil { + return xerrors.Errorf(`"docker" not found in $PATH`) + } + return nil +} + +func NewRunContainer(ctx context.Context, config *ContainerConfig) (*RunContainer, error) { + if err := preflightChecks(); err != nil { + return nil, err + } + + args := []string{ + "run", + "--name", config.Name, "-it", "-d", - image, - ) + } + args = append(args, mountArgs(config.Mounts)...) + args = append(args, config.Image) + + cmd := exec.CommandContext(ctx, "docker", args...) out, err := cmd.CombinedOutput() if err != nil { return nil, xerrors.Errorf( "failed to start testing container %q, (%s): %w", - name, string(out), err) + config.Name, string(out), err) } return &RunContainer{ - name: name, + name: config.Name, ctx: ctx, }, nil } @@ -57,12 +85,18 @@ func (r *RunContainer) Close() error { } type Assertable struct { - cmd string + cmd *exec.Cmd ctx context.Context container *RunContainer } -func (r *RunContainer) Run(ctx context.Context, cmd string) *Assertable { +// Run executes the given command in the runtime container with reasonable defaults +func (r *RunContainer) Run(ctx context.Context, command string) *Assertable { + cmd := exec.CommandContext(ctx, + "docker", "exec", "-i", r.name, + "sh", "-c", command, + ) + return &Assertable{ cmd: cmd, ctx: ctx, @@ -70,23 +104,32 @@ func (r *RunContainer) Run(ctx context.Context, cmd string) *Assertable { } } +// RunCmd lifts the given *exec.Cmd into the runtime container +func (r *RunContainer) RunCmd(cmd *exec.Cmd) *Assertable { + path, _ := exec.LookPath("docker") + cmd.Path = path + command := strings.Join(cmd.Args, " ") + cmd.Args = append([]string{"docker", "exec", "-i", r.name, "sh", "-c", command}) + + return &Assertable{ + cmd: cmd, + container: r, + } +} + func (a Assertable) Assert(t *testing.T, option ...Assertion) { var cmdResult CommandResult - cmd := exec.CommandContext(a.ctx, - "docker", "exec", a.container.name, - "sh", "-c", a.cmd, - ) var ( stdout bytes.Buffer stderr bytes.Buffer ) - cmd.Stdout = &stdout - cmd.Stderr = &stderr + a.cmd.Stdout = &stdout + a.cmd.Stderr = &stderr start := time.Now() - err := cmd.Run() + err := a.cmd.Run() cmdResult.Duration = time.Since(start) if exitErr, ok := err.(*exec.ExitError); ok { @@ -147,7 +190,7 @@ func ExitCodeIs(code int) Assertion { return simpleFuncAssert{ valid: func(r CommandResult) error { if r.ExitCode != code { - return xerrors.Errorf("exit code of %s expected, got %v", code, r.ExitCode) + return xerrors.Errorf("exit code of %v expected, got %v", code, r.ExitCode) } return nil }, @@ -209,7 +252,10 @@ func matches(name, pattern string, target []byte) error { return xerrors.Errorf("failed to attempt regexp match: %w", err) } if !ok { - return xerrors.Errorf("expected to find pattern (%s) in %s, no match found", pattern, name) + return xerrors.Errorf( + "expected to find pattern (%s) in %s, no match found in (%v)", + pattern, name, string(target), + ) } return nil } From 025e1bdfc1e512997560264b5648eea69cdb61cc Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 28 Jul 2020 10:00:36 -0500 Subject: [PATCH 04/19] Adds working bind mount --- .gitignore | 1 + ci/integration/integration_test.go | 30 ++++++++++++++++++++++++++++++ ci/tcli/tcli.go | 10 +++++----- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 5fd924f8..05e4aa48 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea ci/bin cmd/coder/coder +ci/integration/bin diff --git a/ci/integration/integration_test.go b/ci/integration/integration_test.go index 7b8a90e5..1813ed0f 100644 --- a/ci/integration/integration_test.go +++ b/ci/integration/integration_test.go @@ -2,7 +2,10 @@ package integration import ( "context" + "fmt" + "os" "os/exec" + "path/filepath" "strings" "testing" "time" @@ -11,12 +14,33 @@ import ( "cdr.dev/slog/sloggers/slogtest/assert" ) +func build(t *testing.T, path string) { + cmd := exec.Command( + "sh", "-c", + fmt.Sprintf("cd ../../ && go build -o %s ./cmd/coder", path), + ) + cmd.Env = append(os.Environ(), "GOOS=linux", "CGO_ENABLED=0") + + out, err := cmd.CombinedOutput() + t.Logf("%s", string(out)) + assert.Success(t, "build go binary", err) +} + func TestTCli(t *testing.T) { ctx := context.Background() + cwd, err := os.Getwd() + assert.Success(t, "get working dir", err) + + binpath := filepath.Join(cwd, "bin", "coder") + build(t, binpath) + container, err := tcli.NewRunContainer(ctx, &tcli.ContainerConfig{ Image: "ubuntu:latest", Name: "test-container", + BindMounts: map[string]string{ + binpath: "/bin/coder", + }, }) assert.Success(t, "new run container", err) @@ -43,4 +67,10 @@ func TestTCli(t *testing.T) { tcli.StderrEmpty(), tcli.StdoutMatches("testing"), ) + + container.Run(ctx, "which coder").Assert(t, + tcli.Success(), + tcli.StdoutMatches("/bin/coder"), + tcli.StderrEmpty(), + ) } diff --git a/ci/tcli/tcli.go b/ci/tcli/tcli.go index 40d7ac6d..9b9cd95a 100644 --- a/ci/tcli/tcli.go +++ b/ci/tcli/tcli.go @@ -20,14 +20,14 @@ type RunContainer struct { } type ContainerConfig struct { - Name string - Image string - Mounts map[string]string + Name string + Image string + BindMounts map[string]string } func mountArgs(m map[string]string) (args []string) { for src, dest := range m { - args = append(args, "--mount", fmt.Sprintf("source=%s,target=%s", src, dest)) + args = append(args, "--mount", fmt.Sprintf("type=bind,source=%s,target=%s", src, dest)) } return args } @@ -50,7 +50,7 @@ func NewRunContainer(ctx context.Context, config *ContainerConfig) (*RunContaine "--name", config.Name, "-it", "-d", } - args = append(args, mountArgs(config.Mounts)...) + args = append(args, mountArgs(config.BindMounts)...) args = append(args, config.Image) cmd := exec.CommandContext(ctx, "docker", args...) From f11304ba32c6d27f9d0caf875e48163890440a92 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 28 Jul 2020 10:32:40 -0500 Subject: [PATCH 05/19] Adds github fmt, lint, test ci --- .github/workflows/build.yaml | 2 +- .github/workflows/test.yaml | 46 ++++++++++++++++++++++++++++++++++++ ci/image/Dockerfile | 8 +++++++ ci/{ => steps}/build.sh | 4 ++-- ci/steps/fmt.sh | 16 +++++++++++++ ci/steps/lint.sh | 6 +++++ 6 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/test.yaml create mode 100644 ci/image/Dockerfile rename ci/{ => steps}/build.sh (94%) create mode 100755 ci/steps/fmt.sh create mode 100755 ci/steps/lint.sh diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index d246670f..11dec656 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -9,7 +9,7 @@ jobs: - name: Checkout uses: actions/checkout@v1 - name: Build - run: ./ci/build.sh + run: ./ci/steps/build.sh - name: Upload uses: actions/upload-artifact@v2 with: diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 00000000..d169aec5 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,46 @@ +name: ci +on: [push, pull_request] + +jobs: + fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions/cache@v1 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: fmt + uses: ./ci/image + with: + args: ./ci/steps/fmt.sh + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions/cache@v1 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: lint + uses: ./ci/image + with: + args: ./ci/steps/lint.sh + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions/cache@v1 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: test + uses: ./ci/image + with: + args: go test ./... diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile new file mode 100644 index 00000000..cb06bf81 --- /dev/null +++ b/ci/image/Dockerfile @@ -0,0 +1,8 @@ +FROM golang:1 + +ENV GOFLAGS="-mod=readonly" +ENV CI=true + +RUN go get golang.org/x/tools/cmd/goimports +RUN go get golang.org/x/lint/golint +RUN go get github.com/mattn/goveralls \ No newline at end of file diff --git a/ci/build.sh b/ci/steps/build.sh similarity index 94% rename from ci/build.sh rename to ci/steps/build.sh index 8a6f540c..8411ccde 100755 --- a/ci/build.sh +++ b/ci/steps/build.sh @@ -14,14 +14,14 @@ mkdir -p bin build(){ tmpdir=$(mktemp -d) - go build -ldflags "-s -w -X main.version=${tag}" -o "$tmpdir/coder" ../cmd/coder + go build -ldflags "-s -w -X main.version=${tag}" -o "$tmpdir/coder" ../../cmd/coder pushd "$tmpdir" tarname="coder-cli-$GOOS-$GOARCH.tar.gz" tar -czf "$tarname" coder popd - cp "$tmpdir/$tarname" bin + cp "$tmpdir/$tarname" ../bin rm -rf "$tmpdir" } diff --git a/ci/steps/fmt.sh b/ci/steps/fmt.sh new file mode 100755 index 00000000..8309a2c3 --- /dev/null +++ b/ci/steps/fmt.sh @@ -0,0 +1,16 @@ +#!/bin/bash +echo "Formatting..." + +go mod tidy +gofmt -w -s . +goimports -w "-local=$$(go list -m)" . + +if [ "$CI" != "" ]; then + if [[ $(git ls-files --other --modified --exclude-standard) != "" ]]; then + echo "Files need generation or are formatted incorrectly:" + git -c color.ui=always status | grep --color=no '\e\[31m' + echo "Please run the following locally:" + echo " ./ci/fmt.sh" + exit 1 + fi +fi \ No newline at end of file diff --git a/ci/steps/lint.sh b/ci/steps/lint.sh new file mode 100755 index 00000000..51da081d --- /dev/null +++ b/ci/steps/lint.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +echo "Linting..." + +go vet ./... +golint -set_exit_status ./... \ No newline at end of file From 5998ff13adca65de5c99bcc3272d15bcc290e66f Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 28 Jul 2020 11:13:17 -0500 Subject: [PATCH 06/19] Add integration action --- .github/workflows/integration.yaml | 23 +++++++++++++++++++++++ .github/workflows/test.yaml | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/integration.yaml diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml new file mode 100644 index 00000000..cff8699a --- /dev/null +++ b/.github/workflows/integration.yaml @@ -0,0 +1,23 @@ +name: ci +on: [push, pull_request] + +jobs: + integration: + runs-on: ubuntu-latest + env: + CODER_URL: ${{ secrets.CODER_URL }} + CODER_EMAIL: ${{ secrets.CODER_EMAIL }} + CODER_PASSWORD: ${{ secrets.CODER_PASSWORD }} + steps: + - uses: actions/checkout@v1 + - uses: actions/cache@v1 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - uses: actions/setup-go@v2 + with: + go-version: '^1.14' + - name: go test + run: go test ./ci/integration/... diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d169aec5..fb7e32db 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -43,4 +43,4 @@ jobs: - name: test uses: ./ci/image with: - args: go test ./... + args: go test ./internal/... ./cmd/... From 149586748b7e795d90756f7884625e09aaf2e4d4 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 28 Jul 2020 13:55:36 -0500 Subject: [PATCH 07/19] Add initial coder tests --- ci/integration/integration_test.go | 63 +++++++++++++++++++++++++----- ci/tcli/tcli.go | 59 +++++++++++++++------------- 2 files changed, 84 insertions(+), 38 deletions(-) diff --git a/ci/integration/integration_test.go b/ci/integration/integration_test.go index 1813ed0f..9eb1d8ef 100644 --- a/ci/integration/integration_test.go +++ b/ci/integration/integration_test.go @@ -14,26 +14,37 @@ import ( "cdr.dev/slog/sloggers/slogtest/assert" ) -func build(t *testing.T, path string) { +func build(path string) error { cmd := exec.Command( "sh", "-c", fmt.Sprintf("cd ../../ && go build -o %s ./cmd/coder", path), ) cmd.Env = append(os.Environ(), "GOOS=linux", "CGO_ENABLED=0") - out, err := cmd.CombinedOutput() - t.Logf("%s", string(out)) - assert.Success(t, "build go binary", err) + _, err := cmd.CombinedOutput() + if err != nil { + return err + } + return nil } -func TestTCli(t *testing.T) { - ctx := context.Background() +var binpath string +func init() { cwd, err := os.Getwd() - assert.Success(t, "get working dir", err) + if err != nil { + panic(err) + } + + binpath = filepath.Join(cwd, "bin", "coder") + err = build(binpath) + if err != nil { + panic(err) + } +} - binpath := filepath.Join(cwd, "bin", "coder") - build(t, binpath) +func TestTCli(t *testing.T) { + ctx := context.Background() container, err := tcli.NewRunContainer(ctx, &tcli.ContainerConfig{ Image: "ubuntu:latest", @@ -42,7 +53,6 @@ func TestTCli(t *testing.T) { binpath: "/bin/coder", }, }) - assert.Success(t, "new run container", err) defer container.Close() @@ -73,4 +83,37 @@ func TestTCli(t *testing.T) { tcli.StdoutMatches("/bin/coder"), tcli.StderrEmpty(), ) + + container.Run(ctx, "coder version").Assert(t, + tcli.StderrEmpty(), + tcli.Success(), + tcli.StdoutMatches("linux"), + ) +} + +func TestCoderCLI(t *testing.T) { + ctx := context.Background() + + c, err := tcli.NewRunContainer(ctx, &tcli.ContainerConfig{ + Image: "ubuntu:latest", + Name: "test-container", + BindMounts: map[string]string{ + binpath: "/bin/coder", + }, + }) + assert.Success(t, "new run container", err) + defer c.Close() + + c.Run(ctx, "coder version").Assert(t, + tcli.StderrEmpty(), + tcli.Success(), + tcli.StdoutMatches("linux"), + ) + + c.Run(ctx, "coder help").Assert(t, + tcli.Success(), + tcli.StderrMatches("Commands:"), + tcli.StderrMatches("Usage: coder"), + tcli.StdoutEmpty(), + ) } diff --git a/ci/tcli/tcli.go b/ci/tcli/tcli.go index 9b9cd95a..b5f35404 100644 --- a/ci/tcli/tcli.go +++ b/ci/tcli/tcli.go @@ -118,41 +118,44 @@ func (r *RunContainer) RunCmd(cmd *exec.Cmd) *Assertable { } func (a Assertable) Assert(t *testing.T, option ...Assertion) { - var cmdResult CommandResult + t.Run(strings.Join(a.cmd.Args[6:], " "), func(t *testing.T) { + var cmdResult CommandResult - var ( - stdout bytes.Buffer - stderr bytes.Buffer - ) + var ( + stdout bytes.Buffer + stderr bytes.Buffer + ) - a.cmd.Stdout = &stdout - a.cmd.Stderr = &stderr + a.cmd.Stdout = &stdout + a.cmd.Stderr = &stderr - start := time.Now() - err := a.cmd.Run() - cmdResult.Duration = time.Since(start) + start := time.Now() + err := a.cmd.Run() + cmdResult.Duration = time.Since(start) - if exitErr, ok := err.(*exec.ExitError); ok { - cmdResult.ExitCode = exitErr.ExitCode() - } else if err != nil { - cmdResult.ExitCode = -1 - } else { - cmdResult.ExitCode = 0 - } + if exitErr, ok := err.(*exec.ExitError); ok { + cmdResult.ExitCode = exitErr.ExitCode() + } else if err != nil { + cmdResult.ExitCode = -1 + } else { + cmdResult.ExitCode = 0 + } - cmdResult.Stdout = stdout.Bytes() - cmdResult.Stderr = stderr.Bytes() + cmdResult.Stdout = stdout.Bytes() + cmdResult.Stderr = stderr.Bytes() - for ix, o := range option { - name := fmt.Sprintf("assertion_#%v", ix) - if named, ok := o.(Named); ok { - name = named.Name() + for ix, o := range option { + name := fmt.Sprintf("assertion_#%v", ix) + if named, ok := o.(Named); ok { + name = named.Name() + } + t.Run(name, func(t *testing.T) { + t.Parallel() + err := o.Valid(cmdResult) + assert.Success(t, name, err) + }) } - t.Run(name, func(t *testing.T) { - err := o.Valid(cmdResult) - assert.Success(t, name, err) - }) - } + }) } type Assertion interface { From a7ac5237504c5bc8b38b72302902ca2a34c80e5a Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 28 Jul 2020 14:54:39 -0500 Subject: [PATCH 08/19] Doc comments only --- ci/steps/fmt.sh | 2 +- ci/tcli/tcli.go | 20 ++++++++++++++++++++ cmd/coder/sync.go | 7 +++---- internal/activity/pusher.go | 5 ++++- internal/activity/writer.go | 2 ++ internal/config/file.go | 5 +++++ internal/entclient/activity.go | 1 + internal/entclient/client.go | 1 + internal/entclient/devurl.go | 10 ++++++---- internal/entclient/env.go | 4 ++++ internal/entclient/me.go | 4 ++++ internal/entclient/org.go | 2 ++ internal/loginsrv/server.go | 1 + internal/sync/sync.go | 1 + internal/xterminal/terminal.go | 1 + 15 files changed, 56 insertions(+), 10 deletions(-) diff --git a/ci/steps/fmt.sh b/ci/steps/fmt.sh index 8309a2c3..bb4b0d2c 100755 --- a/ci/steps/fmt.sh +++ b/ci/steps/fmt.sh @@ -10,7 +10,7 @@ if [ "$CI" != "" ]; then echo "Files need generation or are formatted incorrectly:" git -c color.ui=always status | grep --color=no '\e\[31m' echo "Please run the following locally:" - echo " ./ci/fmt.sh" + echo " ./ci/steps/fmt.sh" exit 1 fi fi \ No newline at end of file diff --git a/ci/tcli/tcli.go b/ci/tcli/tcli.go index b5f35404..574e3bf8 100644 --- a/ci/tcli/tcli.go +++ b/ci/tcli/tcli.go @@ -14,11 +14,13 @@ import ( "golang.org/x/xerrors" ) +// RunContainer specifies a runtime container for performing command tests type RunContainer struct { name string ctx context.Context } +// ContainerConfig describes the RunContainer configuration schema for initializing a testing environment type ContainerConfig struct { Name string Image string @@ -40,6 +42,7 @@ func preflightChecks() error { return nil } +// NewRunContainer starts a new docker container for executing command tests func NewRunContainer(ctx context.Context, config *ContainerConfig) (*RunContainer, error) { if err := preflightChecks(); err != nil { return nil, err @@ -68,6 +71,7 @@ func NewRunContainer(ctx context.Context, config *ContainerConfig) (*RunContaine }, nil } +// Close kills and removes the command execution testing container func (r *RunContainer) Close() error { cmd := exec.CommandContext(r.ctx, "sh", "-c", strings.Join([]string{ @@ -84,6 +88,7 @@ func (r *RunContainer) Close() error { return nil } +// Assertable describes an initialized command ready to be run and asserted against type Assertable struct { cmd *exec.Cmd ctx context.Context @@ -117,6 +122,7 @@ func (r *RunContainer) RunCmd(cmd *exec.Cmd) *Assertable { } } +// Assert runs the Assertable and func (a Assertable) Assert(t *testing.T, option ...Assertion) { t.Run(strings.Join(a.cmd.Args[6:], " "), func(t *testing.T) { var cmdResult CommandResult @@ -158,14 +164,19 @@ func (a Assertable) Assert(t *testing.T, option ...Assertion) { }) } +// Assertion specifies an assertion on the given CommandResult. +// Pass custom Assertion types to cover special cases. type Assertion interface { Valid(r CommandResult) error } +// Named is an optional extension of Assertion that provides a helpful label +// to *testing.T type Named interface { Name() string } +// CommandResult contains the aggregated result of a command execution type CommandResult struct { Stdout, Stderr []byte ExitCode int @@ -185,10 +196,12 @@ func (s simpleFuncAssert) Name() string { return s.name } +// Success asserts that the command exited with an exit code of 0 func Success() Assertion { return ExitCodeIs(0) } +// ExitCodeIs asserts that the command exited with the given code func ExitCodeIs(code int) Assertion { return simpleFuncAssert{ valid: func(r CommandResult) error { @@ -201,6 +214,7 @@ func ExitCodeIs(code int) Assertion { } } +// StdoutEmpty asserts that the command did not write any data to Stdout func StdoutEmpty() Assertion { return simpleFuncAssert{ valid: func(r CommandResult) error { @@ -210,6 +224,7 @@ func StdoutEmpty() Assertion { } } +// StderrEmpty asserts that the command did not write any data to Stderr func StderrEmpty() Assertion { return simpleFuncAssert{ valid: func(r CommandResult) error { @@ -219,6 +234,7 @@ func StderrEmpty() Assertion { } } +// StdoutMatches asserts that Stdout contains a substring which matches the given regexp func StdoutMatches(pattern string) Assertion { return simpleFuncAssert{ valid: func(r CommandResult) error { @@ -228,6 +244,7 @@ func StdoutMatches(pattern string) Assertion { } } +// StderrMatches asserts that Stderr contains a substring which matches the given regexp func StderrMatches(pattern string) Assertion { return simpleFuncAssert{ valid: func(r CommandResult) error { @@ -237,6 +254,7 @@ func StderrMatches(pattern string) Assertion { } } +// CombinedMatches asserts that either Stdout or Stderr a substring which matches the given regexp func CombinedMatches(pattern string) Assertion { return simpleFuncAssert{ valid: func(r CommandResult) error { @@ -270,6 +288,7 @@ func empty(name string, a []byte) error { return nil } +// DurationLessThan asserts that the command completed in less than the given duration func DurationLessThan(dur time.Duration) Assertion { return simpleFuncAssert{ valid: func(r CommandResult) error { @@ -282,6 +301,7 @@ func DurationLessThan(dur time.Duration) Assertion { } } +// DurationGreaterThan asserts that the command completed in greater than the given duration func DurationGreaterThan(dur time.Duration) Assertion { return simpleFuncAssert{ valid: func(r CommandResult) error { diff --git a/cmd/coder/sync.go b/cmd/coder/sync.go index e0d62708..601fddd7 100644 --- a/cmd/coder/sync.go +++ b/cmd/coder/sync.go @@ -2,7 +2,6 @@ package main import ( "bytes" - "fmt" "log" "os" "os/exec" @@ -34,7 +33,7 @@ func (cmd *syncCmd) RegisterFlags(fl *pflag.FlagSet) { } // version returns local rsync protocol version as a string. -func (_ *syncCmd) version() string { +func rsyncVersion() string { cmd := exec.Command("rsync", "--version") out, err := cmd.CombinedOutput() if err != nil { @@ -93,13 +92,13 @@ func (cmd *syncCmd) Run(fl *pflag.FlagSet) { Client: entClient, } - localVersion := cmd.version() + localVersion := rsyncVersion() remoteVersion, rsyncErr := s.Version() if rsyncErr != nil { flog.Info("Unable to determine remote rsync version. Proceeding cautiously.") } else if localVersion != remoteVersion { - flog.Fatal(fmt.Sprintf("rsync protocol mismatch. %s.", localVersion, rsyncErr)) + flog.Fatal("rsync protocol mismatch: local = %v, remote = %v", localVersion, rsyncErr) } for err == nil || err == sync.ErrRestartSync { diff --git a/internal/activity/pusher.go b/internal/activity/pusher.go index fb57068d..c2102fc1 100644 --- a/internal/activity/pusher.go +++ b/internal/activity/pusher.go @@ -4,8 +4,9 @@ import ( "time" "cdr.dev/coder-cli/internal/entclient" - "go.coder.com/flog" "golang.org/x/time/rate" + + "go.coder.com/flog" ) const pushInterval = time.Minute @@ -20,6 +21,7 @@ type Pusher struct { rate *rate.Limiter } +// NewPusher instantiates a new instance of Pusher func NewPusher(c *entclient.Client, envID, source string) *Pusher { return &Pusher{ envID: envID, @@ -29,6 +31,7 @@ func NewPusher(c *entclient.Client, envID, source string) *Pusher { } } +// Push pushes activity, abiding by a rate limit func (p *Pusher) Push() { if !p.rate.Allow() { return diff --git a/internal/activity/writer.go b/internal/activity/writer.go index 1e5c4f66..a10c4341 100644 --- a/internal/activity/writer.go +++ b/internal/activity/writer.go @@ -7,11 +7,13 @@ type activityWriter struct { wr io.Writer } +// Write writes to the underlying writer and tracks activity func (w *activityWriter) Write(p []byte) (n int, err error) { w.p.Push() return w.wr.Write(p) } +// Writer wraps the given writer such that all writes trigger an activity push func (p *Pusher) Writer(wr io.Writer) io.Writer { return &activityWriter{p: p, wr: wr} } diff --git a/internal/config/file.go b/internal/config/file.go index de254fc7..bb83e746 100644 --- a/internal/config/file.go +++ b/internal/config/file.go @@ -1,20 +1,25 @@ package config +// File provides convenience methods for interacting with *os.File type File string +// Delete deletes the file func (f File) Delete() error { return rm(string(f)) } +// Write writes the string to the file func (f File) Write(s string) error { return write(string(f), 0600, []byte(s)) } +// Read reads the file to a string func (f File) Read() (string, error) { byt, err := read(string(f)) return string(byt), err } +// Coder CLI configuration files var ( Session File = "session" URL File = "url" diff --git a/internal/entclient/activity.go b/internal/entclient/activity.go index 92c0afed..362d7da0 100644 --- a/internal/entclient/activity.go +++ b/internal/entclient/activity.go @@ -4,6 +4,7 @@ import ( "net/http" ) +// PushActivity pushes CLI activity to Coder func (c Client) PushActivity(source string, envID string) error { res, err := c.request("POST", "/api/metrics/usage/push", map[string]string{ "source": source, diff --git a/internal/entclient/client.go b/internal/entclient/client.go index e0609d13..a306c707 100644 --- a/internal/entclient/client.go +++ b/internal/entclient/client.go @@ -6,6 +6,7 @@ import ( "net/url" ) +// Client wraps the Coder HTTP API type Client struct { BaseURL *url.URL Token string diff --git a/internal/entclient/devurl.go b/internal/entclient/devurl.go index 57925372..9260dcce 100644 --- a/internal/entclient/devurl.go +++ b/internal/entclient/devurl.go @@ -5,11 +5,12 @@ import ( "net/http" ) +// DelDevURL deletes the specified devurl func (c Client) DelDevURL(envID, urlID string) error { reqString := "/api/environments/%s/devurls/%s" - reqUrl := fmt.Sprintf(reqString, envID, urlID) + reqURL := fmt.Sprintf(reqString, envID, urlID) - res, err := c.request("DELETE", reqUrl, map[string]string{ + res, err := c.request("DELETE", reqURL, map[string]string{ "environment_id": envID, "url_id": urlID, }) @@ -24,11 +25,12 @@ func (c Client) DelDevURL(envID, urlID string) error { return nil } +// UpsertDevURL upserts the specified devurl for the authenticated user func (c Client) UpsertDevURL(envID, port, access string) error { reqString := "/api/environments/%s/devurls" - reqUrl := fmt.Sprintf(reqString, envID) + reqURL := fmt.Sprintf(reqString, envID) - res, err := c.request("POST", reqUrl, map[string]string{ + res, err := c.request("POST", reqURL, map[string]string{ "environment_id": envID, "port": port, "access": access, diff --git a/internal/entclient/env.go b/internal/entclient/env.go index 45a7aa0e..11a806c5 100644 --- a/internal/entclient/env.go +++ b/internal/entclient/env.go @@ -7,11 +7,13 @@ import ( "nhooyr.io/websocket" ) +// Environment describes a Coder environment type Environment struct { Name string `json:"name"` ID string `json:"id"` } +// Envs gets the list of environments owned by the authenticated user func (c Client) Envs(user *User, org Org) ([]Environment, error) { var envs []Environment err := c.requestBody( @@ -22,6 +24,8 @@ func (c Client) Envs(user *User, org Org) ([]Environment, error) { return envs, err } +// DialWsep dials an environments command execution interface +// See github.com/cdr/wsep for details func (c Client) DialWsep(ctx context.Context, env Environment) (*websocket.Conn, error) { u := c.copyURL() if c.BaseURL.Scheme == "https" { diff --git a/internal/entclient/me.go b/internal/entclient/me.go index 7c7c66f0..69c3bde5 100644 --- a/internal/entclient/me.go +++ b/internal/entclient/me.go @@ -1,11 +1,13 @@ package entclient +// User describes a Coder user account type User struct { ID string `json:"id"` Email string `json:"email"` Username string `json:"username"` } +// Me gets the details of the authenticated user func (c Client) Me() (*User, error) { var u User err := c.requestBody("GET", "/api/users/me", nil, &u) @@ -15,11 +17,13 @@ func (c Client) Me() (*User, error) { return &u, nil } +// SSHKey describes an SSH keypair type SSHKey struct { PublicKey string `json:"public_key"` PrivateKey string `json:"private_key"` } +// SSHKey gets the current SSH kepair of the authenticated user func (c Client) SSHKey() (*SSHKey, error) { var key SSHKey err := c.requestBody("GET", "/api/users/me/sshkey", nil, &key) diff --git a/internal/entclient/org.go b/internal/entclient/org.go index 24a8307b..6ac9cdc5 100644 --- a/internal/entclient/org.go +++ b/internal/entclient/org.go @@ -1,11 +1,13 @@ package entclient +// Org describes an Organization in Coder type Org struct { ID string `json:"id"` Name string `json:"name"` Members []User `json:"members"` } +// Orgs gets all Organizations func (c Client) Orgs() ([]Org, error) { var os []Org err := c.requestBody("GET", "/api/orgs", nil, &os) diff --git a/internal/loginsrv/server.go b/internal/loginsrv/server.go index b062977a..2f8774c9 100644 --- a/internal/loginsrv/server.go +++ b/internal/loginsrv/server.go @@ -6,6 +6,7 @@ import ( "sync" ) +// Server waits for the login callback to send session token type Server struct { TokenCond *sync.Cond Token string diff --git a/internal/sync/sync.go b/internal/sync/sync.go index b00cbeb1..b58382a1 100644 --- a/internal/sync/sync.go +++ b/internal/sync/sync.go @@ -210,6 +210,7 @@ func (s Sync) work(ev timedEvent) { } } +// ErrRestartSync describes a known error case that can be solved by re-starting the command var ErrRestartSync = errors.New("the sync exited because it was overloaded, restart it") // workEventGroup converges a group of events to prevent duplicate work. diff --git a/internal/xterminal/terminal.go b/internal/xterminal/terminal.go index d2838725..ce3c9e3f 100644 --- a/internal/xterminal/terminal.go +++ b/internal/xterminal/terminal.go @@ -41,6 +41,7 @@ func ColorEnabled(fd uintptr) (bool, error) { return terminal.IsTerminal(int(fd)), nil } +// ResizeEvent describes the new terminal dimensions following a resize type ResizeEvent struct { Height, Width uint16 } From 5c118b8b0ab908ecdecab3f2043c4e54e127df77 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 28 Jul 2020 15:29:53 -0500 Subject: [PATCH 09/19] Limit github action runs --- .github/workflows/build.yaml | 1 - .github/workflows/integration.yaml | 7 +++++-- .github/workflows/test.yaml | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 11dec656..50cdd7d9 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,5 +1,4 @@ name: build - on: [push] jobs: diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index cff8699a..ae0121a4 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -1,5 +1,8 @@ -name: ci -on: [push, pull_request] +name: integration +on: + push: + schedule: + - cron: '*/180 * * * *' jobs: integration: diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index fb7e32db..36397b7d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,5 +1,5 @@ -name: ci -on: [push, pull_request] +name: test +on: [push] jobs: fmt: From 50c4e75b394447f654e74bfbc8789d6f0e947934 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 28 Jul 2020 15:39:30 -0500 Subject: [PATCH 10/19] Add GetResult Assertion --- ci/tcli/tcli.go | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/ci/tcli/tcli.go b/ci/tcli/tcli.go index 574e3bf8..79d26580 100644 --- a/ci/tcli/tcli.go +++ b/ci/tcli/tcli.go @@ -157,7 +157,7 @@ func (a Assertable) Assert(t *testing.T, option ...Assertion) { } t.Run(name, func(t *testing.T) { t.Parallel() - err := o.Valid(cmdResult) + err := o.Valid(&cmdResult) assert.Success(t, name, err) }) } @@ -167,7 +167,7 @@ func (a Assertable) Assert(t *testing.T, option ...Assertion) { // Assertion specifies an assertion on the given CommandResult. // Pass custom Assertion types to cover special cases. type Assertion interface { - Valid(r CommandResult) error + Valid(r *CommandResult) error } // Named is an optional extension of Assertion that provides a helpful label @@ -184,11 +184,11 @@ type CommandResult struct { } type simpleFuncAssert struct { - valid func(r CommandResult) error + valid func(r *CommandResult) error name string } -func (s simpleFuncAssert) Valid(r CommandResult) error { +func (s simpleFuncAssert) Valid(r *CommandResult) error { return s.valid(r) } @@ -204,7 +204,7 @@ func Success() Assertion { // ExitCodeIs asserts that the command exited with the given code func ExitCodeIs(code int) Assertion { return simpleFuncAssert{ - valid: func(r CommandResult) error { + valid: func(r *CommandResult) error { if r.ExitCode != code { return xerrors.Errorf("exit code of %v expected, got %v", code, r.ExitCode) } @@ -217,17 +217,29 @@ func ExitCodeIs(code int) Assertion { // StdoutEmpty asserts that the command did not write any data to Stdout func StdoutEmpty() Assertion { return simpleFuncAssert{ - valid: func(r CommandResult) error { + valid: func(r *CommandResult) error { return empty("stdout", r.Stdout) }, name: fmt.Sprintf("stdout-empty"), } } +// GetResult offers an escape hatch from tcli +// The passed pointer will be assigned to the commands *CommandResult +func GetResult(result **CommandResult) Assertion { + return simpleFuncAssert{ + valid: func(r *CommandResult) error { + *result = r + return nil + }, + name: "get-stdout", + } +} + // StderrEmpty asserts that the command did not write any data to Stderr func StderrEmpty() Assertion { return simpleFuncAssert{ - valid: func(r CommandResult) error { + valid: func(r *CommandResult) error { return empty("stderr", r.Stderr) }, name: fmt.Sprintf("stderr-empty"), @@ -237,7 +249,7 @@ func StderrEmpty() Assertion { // StdoutMatches asserts that Stdout contains a substring which matches the given regexp func StdoutMatches(pattern string) Assertion { return simpleFuncAssert{ - valid: func(r CommandResult) error { + valid: func(r *CommandResult) error { return matches("stdout", pattern, r.Stdout) }, name: fmt.Sprintf("stdout-matches"), @@ -247,7 +259,7 @@ func StdoutMatches(pattern string) Assertion { // StderrMatches asserts that Stderr contains a substring which matches the given regexp func StderrMatches(pattern string) Assertion { return simpleFuncAssert{ - valid: func(r CommandResult) error { + valid: func(r *CommandResult) error { return matches("stderr", pattern, r.Stderr) }, name: fmt.Sprintf("stderr-matches"), @@ -257,7 +269,7 @@ func StderrMatches(pattern string) Assertion { // CombinedMatches asserts that either Stdout or Stderr a substring which matches the given regexp func CombinedMatches(pattern string) Assertion { return simpleFuncAssert{ - valid: func(r CommandResult) error { + valid: func(r *CommandResult) error { //stdoutValid := StdoutMatches(pattern).Valid(r) //stderrValid := StderrMatches(pattern).Valid(r) // TODO: combine errors @@ -291,7 +303,7 @@ func empty(name string, a []byte) error { // DurationLessThan asserts that the command completed in less than the given duration func DurationLessThan(dur time.Duration) Assertion { return simpleFuncAssert{ - valid: func(r CommandResult) error { + valid: func(r *CommandResult) error { if r.Duration > dur { return xerrors.Errorf("expected duration less than %s, took %s", dur.String(), r.Duration.String()) } @@ -304,7 +316,7 @@ func DurationLessThan(dur time.Duration) Assertion { // DurationGreaterThan asserts that the command completed in greater than the given duration func DurationGreaterThan(dur time.Duration) Assertion { return simpleFuncAssert{ - valid: func(r CommandResult) error { + valid: func(r *CommandResult) error { if r.Duration < dur { return xerrors.Errorf("expected duration greater than %s, took %s", dur.String(), r.Duration.String()) } From bf3166656651cdde89f2aee5a1a06b480fa6e139 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 28 Jul 2020 17:44:30 -0500 Subject: [PATCH 11/19] Add host runner --- ci/integration/integration_test.go | 4 +- ci/tcli/tcli.go | 85 ++++++++++++++++++++++-------- 2 files changed, 65 insertions(+), 24 deletions(-) diff --git a/ci/integration/integration_test.go b/ci/integration/integration_test.go index 9eb1d8ef..5fb9f0ac 100644 --- a/ci/integration/integration_test.go +++ b/ci/integration/integration_test.go @@ -46,7 +46,7 @@ func init() { func TestTCli(t *testing.T) { ctx := context.Background() - container, err := tcli.NewRunContainer(ctx, &tcli.ContainerConfig{ + container, err := tcli.NewContainerRunner(ctx, &tcli.ContainerConfig{ Image: "ubuntu:latest", Name: "test-container", BindMounts: map[string]string{ @@ -94,7 +94,7 @@ func TestTCli(t *testing.T) { func TestCoderCLI(t *testing.T) { ctx := context.Background() - c, err := tcli.NewRunContainer(ctx, &tcli.ContainerConfig{ + c, err := tcli.NewContainerRunner(ctx, &tcli.ContainerConfig{ Image: "ubuntu:latest", Name: "test-container", BindMounts: map[string]string{ diff --git a/ci/tcli/tcli.go b/ci/tcli/tcli.go index 79d26580..69460c43 100644 --- a/ci/tcli/tcli.go +++ b/ci/tcli/tcli.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "io" "os/exec" "regexp" "strings" @@ -14,13 +15,18 @@ import ( "golang.org/x/xerrors" ) -// RunContainer specifies a runtime container for performing command tests -type RunContainer struct { - name string - ctx context.Context +var ( + _ runnable = &ContainerRunner{} + _ runnable = &HostRunner{} +) + +type runnable interface { + Run(ctx context.Context, command string) *Assertable + RunCmd(cmd *exec.Cmd) *Assertable + io.Closer } -// ContainerConfig describes the RunContainer configuration schema for initializing a testing environment +// ContainerConfig describes the ContainerRunner configuration schema for initializing a testing environment type ContainerConfig struct { Name string Image string @@ -42,8 +48,14 @@ func preflightChecks() error { return nil } -// NewRunContainer starts a new docker container for executing command tests -func NewRunContainer(ctx context.Context, config *ContainerConfig) (*RunContainer, error) { +// ContainerRunner specifies a runtime container for performing command tests +type ContainerRunner struct { + name string + ctx context.Context +} + +// NewContainerRunner starts a new docker container for executing command tests +func NewContainerRunner(ctx context.Context, config *ContainerConfig) (*ContainerRunner, error) { if err := preflightChecks(); err != nil { return nil, err } @@ -65,14 +77,14 @@ func NewRunContainer(ctx context.Context, config *ContainerConfig) (*RunContaine config.Name, string(out), err) } - return &RunContainer{ + return &ContainerRunner{ name: config.Name, ctx: ctx, }, nil } // Close kills and removes the command execution testing container -func (r *RunContainer) Close() error { +func (r *ContainerRunner) Close() error { cmd := exec.CommandContext(r.ctx, "sh", "-c", strings.Join([]string{ "docker", "kill", r.name, "&&", @@ -88,43 +100,72 @@ func (r *RunContainer) Close() error { return nil } +type HostRunner struct{} + +func (r *HostRunner) Run(ctx context.Context, command string) *Assertable { + var ( + args []string + path string + parts = strings.Split(command, " ") + ) + if len(parts) > 0 { + path = parts[0] + } + if len(parts) > 1 { + args = parts[1:] + } + return &Assertable{ + cmd: exec.CommandContext(ctx, path, args...), + tname: command, + } +} + +func (r *HostRunner) RunCmd(cmd *exec.Cmd) *Assertable { + return &Assertable{ + cmd: cmd, + tname: strings.Join(cmd.Args, " "), + } +} + +func (r *HostRunner) Close() error { + return nil +} + // Assertable describes an initialized command ready to be run and asserted against type Assertable struct { - cmd *exec.Cmd - ctx context.Context - container *RunContainer + cmd *exec.Cmd + tname string } // Run executes the given command in the runtime container with reasonable defaults -func (r *RunContainer) Run(ctx context.Context, command string) *Assertable { +func (r *ContainerRunner) Run(ctx context.Context, command string) *Assertable { cmd := exec.CommandContext(ctx, "docker", "exec", "-i", r.name, "sh", "-c", command, ) return &Assertable{ - cmd: cmd, - ctx: ctx, - container: r, + cmd: cmd, + tname: command, } } // RunCmd lifts the given *exec.Cmd into the runtime container -func (r *RunContainer) RunCmd(cmd *exec.Cmd) *Assertable { +func (r *ContainerRunner) RunCmd(cmd *exec.Cmd) *Assertable { path, _ := exec.LookPath("docker") cmd.Path = path command := strings.Join(cmd.Args, " ") - cmd.Args = append([]string{"docker", "exec", "-i", r.name, "sh", "-c", command}) + cmd.Args = []string{"docker", "exec", "-i", r.name, "sh", "-c", command} return &Assertable{ - cmd: cmd, - container: r, + cmd: cmd, + tname: command, } } // Assert runs the Assertable and func (a Assertable) Assert(t *testing.T, option ...Assertion) { - t.Run(strings.Join(a.cmd.Args[6:], " "), func(t *testing.T) { + t.Run(a.tname, func(t *testing.T) { var cmdResult CommandResult var ( @@ -225,7 +266,7 @@ func StdoutEmpty() Assertion { } // GetResult offers an escape hatch from tcli -// The passed pointer will be assigned to the commands *CommandResult +// The pointer passed as "result" will be assigned to the command's *CommandResult func GetResult(result **CommandResult) Assertion { return simpleFuncAssert{ valid: func(r *CommandResult) error { From f4fe567b28e03adc7a0ce932634d090ec0ee61cd Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 28 Jul 2020 17:59:04 -0500 Subject: [PATCH 12/19] Add tests for host runner --- ci/integration/integration_test.go | 22 ++++++++++++++++++++++ ci/tcli/tcli.go | 8 +++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/ci/integration/integration_test.go b/ci/integration/integration_test.go index 5fb9f0ac..82be3697 100644 --- a/ci/integration/integration_test.go +++ b/ci/integration/integration_test.go @@ -91,6 +91,28 @@ func TestTCli(t *testing.T) { ) } +func TestHostRunner(t *testing.T) { + var ( + c tcli.HostRunner + ctx = context.Background() + ) + + c.Run(ctx, "echo testing").Assert(t, + tcli.Success(), + tcli.StderrEmpty(), + tcli.StdoutMatches("testing"), + ) + + wd, err := os.Getwd() + assert.Success(t, "get working dir", err) + + c.Run(ctx, "pwd").Assert(t, + tcli.Success(), + tcli.StderrEmpty(), + tcli.StdoutMatches(wd), + ) +} + func TestCoderCLI(t *testing.T) { ctx := context.Background() diff --git a/ci/tcli/tcli.go b/ci/tcli/tcli.go index 69460c43..f9c012be 100644 --- a/ci/tcli/tcli.go +++ b/ci/tcli/tcli.go @@ -100,8 +100,10 @@ func (r *ContainerRunner) Close() error { return nil } +// HostRunner executes command tests on the host, outside of a container type HostRunner struct{} +// Run executes the given command on the host func (r *HostRunner) Run(ctx context.Context, command string) *Assertable { var ( args []string @@ -114,12 +116,15 @@ func (r *HostRunner) Run(ctx context.Context, command string) *Assertable { if len(parts) > 1 { args = parts[1:] } + cmd := exec.CommandContext(ctx, path, args...) + return &Assertable{ - cmd: exec.CommandContext(ctx, path, args...), + cmd: cmd, tname: command, } } +// RunCmd executes the given command on the host func (r *HostRunner) RunCmd(cmd *exec.Cmd) *Assertable { return &Assertable{ cmd: cmd, @@ -127,6 +132,7 @@ func (r *HostRunner) RunCmd(cmd *exec.Cmd) *Assertable { } } +// Close is a noop for HostRunner func (r *HostRunner) Close() error { return nil } From 9d358a60b23a5906cf2b7faa8ba6c6506cb615e7 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 28 Jul 2020 19:53:54 -0500 Subject: [PATCH 13/19] Adds headless login workaround --- .gitignore | 1 + ci/integration/integration_test.go | 39 ++++++++++++++-- ci/integration/login_test.go | 75 ++++++++++++++++++++++++++++++ ci/tcli/doc.go | 4 ++ ci/tcli/tcli.go | 18 +++++-- 5 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 ci/integration/login_test.go create mode 100644 ci/tcli/doc.go diff --git a/.gitignore b/.gitignore index 05e4aa48..c3d7f6a6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ ci/bin cmd/coder/coder ci/integration/bin +ci/integration/env.sh \ No newline at end of file diff --git a/ci/integration/integration_test.go b/ci/integration/integration_test.go index 82be3697..0fb81a79 100644 --- a/ci/integration/integration_test.go +++ b/ci/integration/integration_test.go @@ -114,11 +114,12 @@ func TestHostRunner(t *testing.T) { } func TestCoderCLI(t *testing.T) { - ctx := context.Background() + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) + defer cancel() c, err := tcli.NewContainerRunner(ctx, &tcli.ContainerConfig{ - Image: "ubuntu:latest", - Name: "test-container", + Image: "codercom/enterprise-dev", + Name: "coder-cli-tests", BindMounts: map[string]string{ binpath: "/bin/coder", }, @@ -138,4 +139,36 @@ func TestCoderCLI(t *testing.T) { tcli.StderrMatches("Usage: coder"), tcli.StdoutEmpty(), ) + + creds := login(ctx, t) + c.Run(ctx, fmt.Sprintf("mkdir -p ~/.config/coder && echo -ne %s > ~/.config/coder/session", creds.token)).Assert(t, + tcli.Success(), + ) + c.Run(ctx, fmt.Sprintf("echo -ne %s > ~/.config/coder/url", creds.url)).Assert(t, + tcli.Success(), + ) + + c.Run(ctx, "coder envs").Assert(t, + tcli.Success(), + ) + + c.Run(ctx, "coder urls").Assert(t, + tcli.Error(), + ) + + c.Run(ctx, "coder sync").Assert(t, + tcli.Error(), + ) + + c.Run(ctx, "coder sh").Assert(t, + tcli.Error(), + ) + + c.Run(ctx, "coder logout").Assert(t, + tcli.Success(), + ) + + c.Run(ctx, "coder envs").Assert(t, + tcli.Error(), + ) } diff --git a/ci/integration/login_test.go b/ci/integration/login_test.go new file mode 100644 index 00000000..e0334f00 --- /dev/null +++ b/ci/integration/login_test.go @@ -0,0 +1,75 @@ +package integration + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "os" + "testing" + + "cdr.dev/slog/sloggers/slogtest/assert" +) + +type credentials struct { + url, token string +} + +func login(ctx context.Context, t *testing.T) credentials { + var ( + email = requireEnv(t, "CODER_EMAIL") + password = requireEnv(t, "CODER_PASSWORD") + rawURL = requireEnv(t, "CODER_URL") + ) + sessionToken := getSessionToken(ctx, t, email, password, rawURL) + + return credentials{ + url: rawURL, + token: sessionToken, + } +} + +func requireEnv(t *testing.T, key string) string { + value := os.Getenv(key) + assert.True(t, fmt.Sprintf("%q is nonempty", key), value != "") + return value +} + +type loginBuiltInAuthReq struct { + Email string `json:"email"` + Password string `json:"password"` +} + +type loginBuiltInAuthResp struct { + SessionToken string `json:"session_token"` +} + +func getSessionToken(ctx context.Context, t *testing.T, email, password, rawURL string) string { + reqbody := loginBuiltInAuthReq{ + Email: email, + Password: password, + } + body, err := json.Marshal(reqbody) + assert.Success(t, "marshal login req body", err) + + u, err := url.Parse(rawURL) + assert.Success(t, "parse raw url", err) + u.Path = "/auth/basic/login" + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewReader(body)) + assert.Success(t, "new request", err) + + resp, err := http.DefaultClient.Do(req) + assert.Success(t, "do request", err) + assert.Equal(t, "request status 201", http.StatusCreated, resp.StatusCode) + + var tokenResp loginBuiltInAuthResp + err = json.NewDecoder(resp.Body).Decode(&tokenResp) + assert.Success(t, "decode response", err) + + defer resp.Body.Close() + + return tokenResp.SessionToken +} diff --git a/ci/tcli/doc.go b/ci/tcli/doc.go new file mode 100644 index 00000000..f82d3df3 --- /dev/null +++ b/ci/tcli/doc.go @@ -0,0 +1,4 @@ +// Package tcli provides a framework for CLI integration testing. +// Execute commands on the raw host of inside docker container. +// Define custom Assertion types to extend test functionality. +package tcli diff --git a/ci/tcli/tcli.go b/ci/tcli/tcli.go index f9c012be..69d30227 100644 --- a/ci/tcli/tcli.go +++ b/ci/tcli/tcli.go @@ -203,7 +203,6 @@ func (a Assertable) Assert(t *testing.T, option ...Assertion) { name = named.Name() } t.Run(name, func(t *testing.T) { - t.Parallel() err := o.Valid(&cmdResult) assert.Success(t, name, err) }) @@ -248,12 +247,25 @@ func Success() Assertion { return ExitCodeIs(0) } +// Error asserts that the command exited with a nonzero exit code +func Error() Assertion { + return simpleFuncAssert{ + valid: func(r *CommandResult) error { + if r.ExitCode == 0 { + return xerrors.Errorf("expected nonzero exit code, got %v", r.ExitCode) + } + return nil + }, + name: fmt.Sprintf("error"), + } +} + // ExitCodeIs asserts that the command exited with the given code func ExitCodeIs(code int) Assertion { return simpleFuncAssert{ valid: func(r *CommandResult) error { if r.ExitCode != code { - return xerrors.Errorf("exit code of %v expected, got %v", code, r.ExitCode) + return xerrors.Errorf("exit code of %v expected, got %v, (%s)", code, r.ExitCode, string(r.Stderr)) } return nil }, @@ -279,7 +291,7 @@ func GetResult(result **CommandResult) Assertion { *result = r return nil }, - name: "get-stdout", + name: "get-result", } } From e6c79f5ca755d2c79cae81716f5b05bea9b022ca Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 28 Jul 2020 20:16:23 -0500 Subject: [PATCH 14/19] Add verbose logging to integration tests --- .github/workflows/integration.yaml | 2 +- ci/integration/integration_test.go | 9 ++++++++- ci/tcli/tcli.go | 10 ++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index ae0121a4..a8045e43 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -23,4 +23,4 @@ jobs: with: go-version: '^1.14' - name: go test - run: go test ./ci/integration/... + run: go test -v ./ci/integration/... diff --git a/ci/integration/integration_test.go b/ci/integration/integration_test.go index 0fb81a79..8bc3f7ce 100644 --- a/ci/integration/integration_test.go +++ b/ci/integration/integration_test.go @@ -44,6 +44,7 @@ func init() { } func TestTCli(t *testing.T) { + t.Parallel() ctx := context.Background() container, err := tcli.NewContainerRunner(ctx, &tcli.ContainerConfig{ @@ -92,6 +93,7 @@ func TestTCli(t *testing.T) { } func TestHostRunner(t *testing.T) { + t.Parallel() var ( c tcli.HostRunner ctx = context.Background() @@ -114,6 +116,7 @@ func TestHostRunner(t *testing.T) { } func TestCoderCLI(t *testing.T) { + t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) defer cancel() @@ -141,7 +144,11 @@ func TestCoderCLI(t *testing.T) { ) creds := login(ctx, t) - c.Run(ctx, fmt.Sprintf("mkdir -p ~/.config/coder && echo -ne %s > ~/.config/coder/session", creds.token)).Assert(t, + cmd := exec.CommandContext(ctx, "mkdir -p ~/.config/coder && cat > ~/.config/coder/session") + + // !IMPORTANT: be careful that this does not appear in logs + cmd.Stdin = strings.NewReader(creds.token) + c.RunCmd(cmd).Assert(t, tcli.Success(), ) c.Run(ctx, fmt.Sprintf("echo -ne %s > ~/.config/coder/url", creds.url)).Assert(t, diff --git a/ci/tcli/tcli.go b/ci/tcli/tcli.go index 69d30227..ea66fce3 100644 --- a/ci/tcli/tcli.go +++ b/ci/tcli/tcli.go @@ -11,6 +11,8 @@ import ( "testing" "time" + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" "cdr.dev/slog/sloggers/slogtest/assert" "golang.org/x/xerrors" ) @@ -197,6 +199,14 @@ func (a Assertable) Assert(t *testing.T, option ...Assertion) { cmdResult.Stdout = stdout.Bytes() cmdResult.Stderr = stderr.Bytes() + slogtest.Info(t, "command output", + slog.F("command", a.cmd), + slog.F("stdout", string(cmdResult.Stdout)), + slog.F("stderr", string(cmdResult.Stderr)), + slog.F("exit-code", cmdResult.ExitCode), + slog.F("duration", cmdResult.Duration), + ) + for ix, o := range option { name := fmt.Sprintf("assertion_#%v", ix) if named, ok := o.(Named); ok { From b5b93d49fa2bfc3a90d7e7559619150ab1523445 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 28 Jul 2020 21:20:31 -0500 Subject: [PATCH 15/19] Fix tcli log helpers --- ci/tcli/tcli.go | 178 +++++++++++++++++++++++++----------------------- 1 file changed, 92 insertions(+), 86 deletions(-) diff --git a/ci/tcli/tcli.go b/ci/tcli/tcli.go index ea66fce3..34141f29 100644 --- a/ci/tcli/tcli.go +++ b/ci/tcli/tcli.go @@ -173,57 +173,49 @@ func (r *ContainerRunner) RunCmd(cmd *exec.Cmd) *Assertable { // Assert runs the Assertable and func (a Assertable) Assert(t *testing.T, option ...Assertion) { - t.Run(a.tname, func(t *testing.T) { - var cmdResult CommandResult + slog.Helper() + var cmdResult CommandResult - var ( - stdout bytes.Buffer - stderr bytes.Buffer - ) + var ( + stdout bytes.Buffer + stderr bytes.Buffer + ) - a.cmd.Stdout = &stdout - a.cmd.Stderr = &stderr - - start := time.Now() - err := a.cmd.Run() - cmdResult.Duration = time.Since(start) - - if exitErr, ok := err.(*exec.ExitError); ok { - cmdResult.ExitCode = exitErr.ExitCode() - } else if err != nil { - cmdResult.ExitCode = -1 - } else { - cmdResult.ExitCode = 0 - } - - cmdResult.Stdout = stdout.Bytes() - cmdResult.Stderr = stderr.Bytes() - - slogtest.Info(t, "command output", - slog.F("command", a.cmd), - slog.F("stdout", string(cmdResult.Stdout)), - slog.F("stderr", string(cmdResult.Stderr)), - slog.F("exit-code", cmdResult.ExitCode), - slog.F("duration", cmdResult.Duration), - ) + a.cmd.Stdout = &stdout + a.cmd.Stderr = &stderr - for ix, o := range option { - name := fmt.Sprintf("assertion_#%v", ix) - if named, ok := o.(Named); ok { - name = named.Name() - } - t.Run(name, func(t *testing.T) { - err := o.Valid(&cmdResult) - assert.Success(t, name, err) - }) - } - }) + start := time.Now() + err := a.cmd.Run() + cmdResult.Duration = time.Since(start) + + if exitErr, ok := err.(*exec.ExitError); ok { + cmdResult.ExitCode = exitErr.ExitCode() + } else if err != nil { + cmdResult.ExitCode = -1 + } else { + cmdResult.ExitCode = 0 + } + + cmdResult.Stdout = stdout.Bytes() + cmdResult.Stderr = stderr.Bytes() + + slogtest.Info(t, "command output", + slog.F("command", a.cmd), + slog.F("stdout", string(cmdResult.Stdout)), + slog.F("stderr", string(cmdResult.Stderr)), + slog.F("exit-code", cmdResult.ExitCode), + slog.F("duration", cmdResult.Duration), + ) + + for _, o := range option { + o.Valid(t, &cmdResult) + } } // Assertion specifies an assertion on the given CommandResult. // Pass custom Assertion types to cover special cases. type Assertion interface { - Valid(r *CommandResult) error + Valid(t *testing.T, r *CommandResult) } // Named is an optional extension of Assertion that provides a helpful label @@ -240,12 +232,13 @@ type CommandResult struct { } type simpleFuncAssert struct { - valid func(r *CommandResult) error + valid func(t *testing.T, r *CommandResult) name string } -func (s simpleFuncAssert) Valid(r *CommandResult) error { - return s.valid(r) +func (s simpleFuncAssert) Valid(t *testing.T, r *CommandResult) { + slog.Helper() + s.valid(t, r) } func (s simpleFuncAssert) Name() string { @@ -254,17 +247,16 @@ func (s simpleFuncAssert) Name() string { // Success asserts that the command exited with an exit code of 0 func Success() Assertion { + slog.Helper() return ExitCodeIs(0) } // Error asserts that the command exited with a nonzero exit code func Error() Assertion { return simpleFuncAssert{ - valid: func(r *CommandResult) error { - if r.ExitCode == 0 { - return xerrors.Errorf("expected nonzero exit code, got %v", r.ExitCode) - } - return nil + valid: func(t *testing.T, r *CommandResult) { + slog.Helper() + assert.True(t, "exit code is nonzero", r.ExitCode != 0) }, name: fmt.Sprintf("error"), } @@ -273,11 +265,9 @@ func Error() Assertion { // ExitCodeIs asserts that the command exited with the given code func ExitCodeIs(code int) Assertion { return simpleFuncAssert{ - valid: func(r *CommandResult) error { - if r.ExitCode != code { - return xerrors.Errorf("exit code of %v expected, got %v, (%s)", code, r.ExitCode, string(r.Stderr)) - } - return nil + valid: func(t *testing.T, r *CommandResult) { + slog.Helper() + assert.Equal(t, "exit code is as expected", code, r.ExitCode) }, name: fmt.Sprintf("exitcode"), } @@ -286,8 +276,9 @@ func ExitCodeIs(code int) Assertion { // StdoutEmpty asserts that the command did not write any data to Stdout func StdoutEmpty() Assertion { return simpleFuncAssert{ - valid: func(r *CommandResult) error { - return empty("stdout", r.Stdout) + valid: func(t *testing.T, r *CommandResult) { + slog.Helper() + empty(t, "stdout", r.Stdout) }, name: fmt.Sprintf("stdout-empty"), } @@ -297,9 +288,10 @@ func StdoutEmpty() Assertion { // The pointer passed as "result" will be assigned to the command's *CommandResult func GetResult(result **CommandResult) Assertion { return simpleFuncAssert{ - valid: func(r *CommandResult) error { + valid: func(t *testing.T, r *CommandResult) { + slog.Helper() + empty(t, "stdout", r.Stdout) *result = r - return nil }, name: "get-result", } @@ -308,8 +300,9 @@ func GetResult(result **CommandResult) Assertion { // StderrEmpty asserts that the command did not write any data to Stderr func StderrEmpty() Assertion { return simpleFuncAssert{ - valid: func(r *CommandResult) error { - return empty("stderr", r.Stderr) + valid: func(t *testing.T, r *CommandResult) { + slog.Helper() + empty(t, "stderr", r.Stderr) }, name: fmt.Sprintf("stderr-empty"), } @@ -318,8 +311,9 @@ func StderrEmpty() Assertion { // StdoutMatches asserts that Stdout contains a substring which matches the given regexp func StdoutMatches(pattern string) Assertion { return simpleFuncAssert{ - valid: func(r *CommandResult) error { - return matches("stdout", pattern, r.Stdout) + valid: func(t *testing.T, r *CommandResult) { + slog.Helper() + matches(t, "stdout", pattern, r.Stdout) }, name: fmt.Sprintf("stdout-matches"), } @@ -328,8 +322,9 @@ func StdoutMatches(pattern string) Assertion { // StderrMatches asserts that Stderr contains a substring which matches the given regexp func StderrMatches(pattern string) Assertion { return simpleFuncAssert{ - valid: func(r *CommandResult) error { - return matches("stderr", pattern, r.Stderr) + valid: func(t *testing.T, r *CommandResult) { + slog.Helper() + matches(t, "stderr", pattern, r.Stderr) }, name: fmt.Sprintf("stderr-matches"), } @@ -338,45 +333,53 @@ func StderrMatches(pattern string) Assertion { // CombinedMatches asserts that either Stdout or Stderr a substring which matches the given regexp func CombinedMatches(pattern string) Assertion { return simpleFuncAssert{ - valid: func(r *CommandResult) error { - //stdoutValid := StdoutMatches(pattern).Valid(r) - //stderrValid := StderrMatches(pattern).Valid(r) - // TODO: combine errors - return nil + valid: func(t *testing.T, r *CommandResult) { + slog.Helper() + StdoutMatches(pattern).Valid(t, r) + StderrMatches(pattern).Valid(t, r) }, name: fmt.Sprintf("combined-matches"), } } -func matches(name, pattern string, target []byte) error { +func matches(t *testing.T, name, pattern string, target []byte) { + slog.Helper() + ok, err := regexp.Match(pattern, target) if err != nil { - return xerrors.Errorf("failed to attempt regexp match: %w", err) + slogtest.Fatal(t, "failed to attempt regexp match", slog.Error(err), + slog.F("pattern", pattern), + slog.F("target", string(target)), + slog.F("sink", name), + ) } if !ok { - return xerrors.Errorf( - "expected to find pattern (%s) in %s, no match found in (%v)", - pattern, name, string(target), + slogtest.Fatal(t, "expected to find pattern, no match found", + slog.F("pattern", pattern), + slog.F("target", string(target)), + slog.F("sink", name), ) } - return nil } -func empty(name string, a []byte) error { +func empty(t *testing.T, name string, a []byte) { + slog.Helper() if len(a) > 0 { - return xerrors.Errorf("expected %s to be empty, got (%s)", name, string(a)) + slogtest.Fatal(t, "expected "+name+" to be empty", slog.F("got", string(a))) } - return nil } // DurationLessThan asserts that the command completed in less than the given duration func DurationLessThan(dur time.Duration) Assertion { return simpleFuncAssert{ - valid: func(r *CommandResult) error { + valid: func(t *testing.T, r *CommandResult) { + slog.Helper() if r.Duration > dur { - return xerrors.Errorf("expected duration less than %s, took %s", dur.String(), r.Duration.String()) + slogtest.Fatal(t, "duration longer than expected", + slog.F("expected_less_than", dur.String), + slog.F("actual", r.Duration.String()), + ) } - return nil }, name: fmt.Sprintf("duration-lessthan"), } @@ -385,11 +388,14 @@ func DurationLessThan(dur time.Duration) Assertion { // DurationGreaterThan asserts that the command completed in greater than the given duration func DurationGreaterThan(dur time.Duration) Assertion { return simpleFuncAssert{ - valid: func(r *CommandResult) error { + valid: func(t *testing.T, r *CommandResult) { + slog.Helper() if r.Duration < dur { - return xerrors.Errorf("expected duration greater than %s, took %s", dur.String(), r.Duration.String()) + slogtest.Fatal(t, "duration shorter than expected", + slog.F("expected_greater_than", dur.String), + slog.F("actual", r.Duration.String()), + ) } - return nil }, name: fmt.Sprintf("duration-greaterthan"), } From c451d16190dcccaff07ffc45a4bc0429b6a49753 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 28 Jul 2020 21:41:15 -0500 Subject: [PATCH 16/19] Move tcli tests to tcli package --- ci/integration/integration_test.go | 78 +++--------------------------- ci/tcli/tcli_test.go | 69 ++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 72 deletions(-) create mode 100644 ci/tcli/tcli_test.go diff --git a/ci/integration/integration_test.go b/ci/integration/integration_test.go index 8bc3f7ce..23e2b5c8 100644 --- a/ci/integration/integration_test.go +++ b/ci/integration/integration_test.go @@ -43,78 +43,6 @@ func init() { } } -func TestTCli(t *testing.T) { - t.Parallel() - ctx := context.Background() - - container, err := tcli.NewContainerRunner(ctx, &tcli.ContainerConfig{ - Image: "ubuntu:latest", - Name: "test-container", - BindMounts: map[string]string{ - binpath: "/bin/coder", - }, - }) - assert.Success(t, "new run container", err) - defer container.Close() - - container.Run(ctx, "echo testing").Assert(t, - tcli.Success(), - tcli.StderrEmpty(), - tcli.StdoutMatches("esting"), - ) - - container.Run(ctx, "sleep 1.5 && echo 1>&2 stderr-message").Assert(t, - tcli.Success(), - tcli.StdoutEmpty(), - tcli.StderrMatches("message"), - tcli.DurationGreaterThan(time.Second), - ) - - cmd := exec.CommandContext(ctx, "cat") - cmd.Stdin = strings.NewReader("testing") - - container.RunCmd(cmd).Assert(t, - tcli.Success(), - tcli.StderrEmpty(), - tcli.StdoutMatches("testing"), - ) - - container.Run(ctx, "which coder").Assert(t, - tcli.Success(), - tcli.StdoutMatches("/bin/coder"), - tcli.StderrEmpty(), - ) - - container.Run(ctx, "coder version").Assert(t, - tcli.StderrEmpty(), - tcli.Success(), - tcli.StdoutMatches("linux"), - ) -} - -func TestHostRunner(t *testing.T) { - t.Parallel() - var ( - c tcli.HostRunner - ctx = context.Background() - ) - - c.Run(ctx, "echo testing").Assert(t, - tcli.Success(), - tcli.StderrEmpty(), - tcli.StdoutMatches("testing"), - ) - - wd, err := os.Getwd() - assert.Success(t, "get working dir", err) - - c.Run(ctx, "pwd").Assert(t, - tcli.Success(), - tcli.StderrEmpty(), - tcli.StdoutMatches(wd), - ) -} - func TestCoderCLI(t *testing.T) { t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) @@ -130,6 +58,12 @@ func TestCoderCLI(t *testing.T) { assert.Success(t, "new run container", err) defer c.Close() + c.Run(ctx, "which coder").Assert(t, + tcli.Success(), + tcli.StdoutMatches("/usr/sbin/coder"), + tcli.StderrEmpty(), + ) + c.Run(ctx, "coder version").Assert(t, tcli.StderrEmpty(), tcli.Success(), diff --git a/ci/tcli/tcli_test.go b/ci/tcli/tcli_test.go new file mode 100644 index 00000000..97bd1b6e --- /dev/null +++ b/ci/tcli/tcli_test.go @@ -0,0 +1,69 @@ +package tcli_test + +import ( + "context" + "os" + "os/exec" + "strings" + "testing" + "time" + + "cdr.dev/coder-cli/ci/tcli" + "cdr.dev/slog/sloggers/slogtest/assert" +) + +func TestTCli(t *testing.T) { + t.Parallel() + ctx := context.Background() + + container, err := tcli.NewContainerRunner(ctx, &tcli.ContainerConfig{ + Image: "ubuntu:latest", + Name: "test-container", + }) + assert.Success(t, "new run container", err) + defer container.Close() + + container.Run(ctx, "echo testing").Assert(t, + tcli.Success(), + tcli.StderrEmpty(), + tcli.StdoutMatches("esting"), + ) + + container.Run(ctx, "sleep 1.5 && echo 1>&2 stderr-message").Assert(t, + tcli.Success(), + tcli.StdoutEmpty(), + tcli.StderrMatches("message"), + tcli.DurationGreaterThan(time.Second), + ) + + cmd := exec.CommandContext(ctx, "cat") + cmd.Stdin = strings.NewReader("testing") + + container.RunCmd(cmd).Assert(t, + tcli.Success(), + tcli.StderrEmpty(), + tcli.StdoutMatches("testing"), + ) +} +func TestHostRunner(t *testing.T) { + t.Parallel() + var ( + c tcli.HostRunner + ctx = context.Background() + ) + + c.Run(ctx, "echo testing").Assert(t, + tcli.Success(), + tcli.StderrEmpty(), + tcli.StdoutMatches("testing"), + ) + + wd, err := os.Getwd() + assert.Success(t, "get working dir", err) + + c.Run(ctx, "pwd").Assert(t, + tcli.Success(), + tcli.StderrEmpty(), + tcli.StdoutMatches(wd), + ) +} From cafdd6f7eb78f4ba4ce2690b6fc1ffd2c603bcd5 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 28 Jul 2020 21:58:10 -0500 Subject: [PATCH 17/19] Simplify assertion type --- ci/integration/integration_test.go | 27 +++-- ci/tcli/tcli.go | 185 ++++++++++------------------- 2 files changed, 76 insertions(+), 136 deletions(-) diff --git a/ci/integration/integration_test.go b/ci/integration/integration_test.go index 23e2b5c8..3fb031be 100644 --- a/ci/integration/integration_test.go +++ b/ci/integration/integration_test.go @@ -43,6 +43,21 @@ func init() { } } +// write session tokens to the given container runner +func headlessLogin(ctx context.Context, t *testing.T, runner *tcli.ContainerRunner) { + creds := login(ctx, t) + cmd := exec.CommandContext(ctx, "mkdir -p ~/.config/coder && cat > ~/.config/coder/session") + + // !IMPORTANT: be careful that this does not appear in logs + cmd.Stdin = strings.NewReader(creds.token) + runner.RunCmd(cmd).Assert(t, + tcli.Success(), + ) + runner.Run(ctx, fmt.Sprintf("echo -ne %s > ~/.config/coder/url", creds.url)).Assert(t, + tcli.Success(), + ) +} + func TestCoderCLI(t *testing.T) { t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) @@ -77,17 +92,7 @@ func TestCoderCLI(t *testing.T) { tcli.StdoutEmpty(), ) - creds := login(ctx, t) - cmd := exec.CommandContext(ctx, "mkdir -p ~/.config/coder && cat > ~/.config/coder/session") - - // !IMPORTANT: be careful that this does not appear in logs - cmd.Stdin = strings.NewReader(creds.token) - c.RunCmd(cmd).Assert(t, - tcli.Success(), - ) - c.Run(ctx, fmt.Sprintf("echo -ne %s > ~/.config/coder/url", creds.url)).Assert(t, - tcli.Success(), - ) + headlessLogin(ctx, t, c) c.Run(ctx, "coder envs").Assert(t, tcli.Success(), diff --git a/ci/tcli/tcli.go b/ci/tcli/tcli.go index 34141f29..de9fab86 100644 --- a/ci/tcli/tcli.go +++ b/ci/tcli/tcli.go @@ -174,11 +174,10 @@ func (r *ContainerRunner) RunCmd(cmd *exec.Cmd) *Assertable { // Assert runs the Assertable and func (a Assertable) Assert(t *testing.T, option ...Assertion) { slog.Helper() - var cmdResult CommandResult - var ( stdout bytes.Buffer stderr bytes.Buffer + result CommandResult ) a.cmd.Stdout = &stdout @@ -186,43 +185,36 @@ func (a Assertable) Assert(t *testing.T, option ...Assertion) { start := time.Now() err := a.cmd.Run() - cmdResult.Duration = time.Since(start) + result.Duration = time.Since(start) if exitErr, ok := err.(*exec.ExitError); ok { - cmdResult.ExitCode = exitErr.ExitCode() + result.ExitCode = exitErr.ExitCode() } else if err != nil { - cmdResult.ExitCode = -1 + // TODO: handle this case better + result.ExitCode = -1 } else { - cmdResult.ExitCode = 0 + result.ExitCode = 0 } - cmdResult.Stdout = stdout.Bytes() - cmdResult.Stderr = stderr.Bytes() + result.Stdout = stdout.Bytes() + result.Stderr = stderr.Bytes() slogtest.Info(t, "command output", slog.F("command", a.cmd), - slog.F("stdout", string(cmdResult.Stdout)), - slog.F("stderr", string(cmdResult.Stderr)), - slog.F("exit-code", cmdResult.ExitCode), - slog.F("duration", cmdResult.Duration), + slog.F("stdout", string(result.Stdout)), + slog.F("stderr", string(result.Stderr)), + slog.F("exit_code", result.ExitCode), + slog.F("duration", result.Duration), ) - for _, o := range option { - o.Valid(t, &cmdResult) + for _, assertion := range option { + assertion(t, &result) } } // Assertion specifies an assertion on the given CommandResult. -// Pass custom Assertion types to cover special cases. -type Assertion interface { - Valid(t *testing.T, r *CommandResult) -} - -// Named is an optional extension of Assertion that provides a helpful label -// to *testing.T -type Named interface { - Name() string -} +// Pass custom Assertion functions to cover special cases. +type Assertion func(t *testing.T, r *CommandResult) // CommandResult contains the aggregated result of a command execution type CommandResult struct { @@ -231,20 +223,6 @@ type CommandResult struct { Duration time.Duration } -type simpleFuncAssert struct { - valid func(t *testing.T, r *CommandResult) - name string -} - -func (s simpleFuncAssert) Valid(t *testing.T, r *CommandResult) { - slog.Helper() - s.valid(t, r) -} - -func (s simpleFuncAssert) Name() string { - return s.name -} - // Success asserts that the command exited with an exit code of 0 func Success() Assertion { slog.Helper() @@ -253,112 +231,75 @@ func Success() Assertion { // Error asserts that the command exited with a nonzero exit code func Error() Assertion { - return simpleFuncAssert{ - valid: func(t *testing.T, r *CommandResult) { - slog.Helper() - assert.True(t, "exit code is nonzero", r.ExitCode != 0) - }, - name: fmt.Sprintf("error"), + return func(t *testing.T, r *CommandResult) { + slog.Helper() + assert.True(t, "exit code is nonzero", r.ExitCode != 0) } } // ExitCodeIs asserts that the command exited with the given code func ExitCodeIs(code int) Assertion { - return simpleFuncAssert{ - valid: func(t *testing.T, r *CommandResult) { - slog.Helper() - assert.Equal(t, "exit code is as expected", code, r.ExitCode) - }, - name: fmt.Sprintf("exitcode"), + return func(t *testing.T, r *CommandResult) { + slog.Helper() + assert.Equal(t, "exit code is as expected", code, r.ExitCode) } } // StdoutEmpty asserts that the command did not write any data to Stdout func StdoutEmpty() Assertion { - return simpleFuncAssert{ - valid: func(t *testing.T, r *CommandResult) { - slog.Helper() - empty(t, "stdout", r.Stdout) - }, - name: fmt.Sprintf("stdout-empty"), + return func(t *testing.T, r *CommandResult) { + slog.Helper() + empty(t, "stdout", r.Stdout) } } // GetResult offers an escape hatch from tcli // The pointer passed as "result" will be assigned to the command's *CommandResult func GetResult(result **CommandResult) Assertion { - return simpleFuncAssert{ - valid: func(t *testing.T, r *CommandResult) { - slog.Helper() - empty(t, "stdout", r.Stdout) - *result = r - }, - name: "get-result", + return func(t *testing.T, r *CommandResult) { + slog.Helper() + *result = r } } // StderrEmpty asserts that the command did not write any data to Stderr func StderrEmpty() Assertion { - return simpleFuncAssert{ - valid: func(t *testing.T, r *CommandResult) { - slog.Helper() - empty(t, "stderr", r.Stderr) - }, - name: fmt.Sprintf("stderr-empty"), + return func(t *testing.T, r *CommandResult) { + slog.Helper() + empty(t, "stderr", r.Stderr) } } // StdoutMatches asserts that Stdout contains a substring which matches the given regexp func StdoutMatches(pattern string) Assertion { - return simpleFuncAssert{ - valid: func(t *testing.T, r *CommandResult) { - slog.Helper() - matches(t, "stdout", pattern, r.Stdout) - }, - name: fmt.Sprintf("stdout-matches"), + return func(t *testing.T, r *CommandResult) { + slog.Helper() + matches(t, "stdout", pattern, r.Stdout) } } // StderrMatches asserts that Stderr contains a substring which matches the given regexp func StderrMatches(pattern string) Assertion { - return simpleFuncAssert{ - valid: func(t *testing.T, r *CommandResult) { - slog.Helper() - matches(t, "stderr", pattern, r.Stderr) - }, - name: fmt.Sprintf("stderr-matches"), - } -} - -// CombinedMatches asserts that either Stdout or Stderr a substring which matches the given regexp -func CombinedMatches(pattern string) Assertion { - return simpleFuncAssert{ - valid: func(t *testing.T, r *CommandResult) { - slog.Helper() - StdoutMatches(pattern).Valid(t, r) - StderrMatches(pattern).Valid(t, r) - }, - name: fmt.Sprintf("combined-matches"), + return func(t *testing.T, r *CommandResult) { + slog.Helper() + matches(t, "stderr", pattern, r.Stderr) } } func matches(t *testing.T, name, pattern string, target []byte) { slog.Helper() + fields := []slog.Field{ + slog.F("pattern", pattern), + slog.F("target", string(target)), + slog.F("sink", name), + } ok, err := regexp.Match(pattern, target) if err != nil { - slogtest.Fatal(t, "failed to attempt regexp match", slog.Error(err), - slog.F("pattern", pattern), - slog.F("target", string(target)), - slog.F("sink", name), - ) + slogtest.Fatal(t, "failed to attempt regexp match", append(fields, slog.Error(err))...) } if !ok { - slogtest.Fatal(t, "expected to find pattern, no match found", - slog.F("pattern", pattern), - slog.F("target", string(target)), - slog.F("sink", name), - ) + slogtest.Fatal(t, "expected to find pattern, no match found", fields...) } } @@ -371,32 +312,26 @@ func empty(t *testing.T, name string, a []byte) { // DurationLessThan asserts that the command completed in less than the given duration func DurationLessThan(dur time.Duration) Assertion { - return simpleFuncAssert{ - valid: func(t *testing.T, r *CommandResult) { - slog.Helper() - if r.Duration > dur { - slogtest.Fatal(t, "duration longer than expected", - slog.F("expected_less_than", dur.String), - slog.F("actual", r.Duration.String()), - ) - } - }, - name: fmt.Sprintf("duration-lessthan"), + return func(t *testing.T, r *CommandResult) { + slog.Helper() + if r.Duration > dur { + slogtest.Fatal(t, "duration longer than expected", + slog.F("expected_less_than", dur.String), + slog.F("actual", r.Duration.String()), + ) + } } } // DurationGreaterThan asserts that the command completed in greater than the given duration func DurationGreaterThan(dur time.Duration) Assertion { - return simpleFuncAssert{ - valid: func(t *testing.T, r *CommandResult) { - slog.Helper() - if r.Duration < dur { - slogtest.Fatal(t, "duration shorter than expected", - slog.F("expected_greater_than", dur.String), - slog.F("actual", r.Duration.String()), - ) - } - }, - name: fmt.Sprintf("duration-greaterthan"), + return func(t *testing.T, r *CommandResult) { + slog.Helper() + if r.Duration < dur { + slogtest.Fatal(t, "duration shorter than expected", + slog.F("expected_greater_than", dur.String), + slog.F("actual", r.Duration.String()), + ) + } } } From 18b5f50abcca7b5c7e79cd46ee93b084be0bf5fd Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Wed, 29 Jul 2020 10:09:20 -0500 Subject: [PATCH 18/19] Cleanup when commands are executed in sh or not --- ci/integration/integration_test.go | 2 +- ci/tcli/doc.go | 2 +- ci/tcli/tcli.go | 71 +++++++++++++----------------- 3 files changed, 33 insertions(+), 42 deletions(-) diff --git a/ci/integration/integration_test.go b/ci/integration/integration_test.go index 3fb031be..e14edd6e 100644 --- a/ci/integration/integration_test.go +++ b/ci/integration/integration_test.go @@ -46,7 +46,7 @@ func init() { // write session tokens to the given container runner func headlessLogin(ctx context.Context, t *testing.T, runner *tcli.ContainerRunner) { creds := login(ctx, t) - cmd := exec.CommandContext(ctx, "mkdir -p ~/.config/coder && cat > ~/.config/coder/session") + cmd := exec.CommandContext(ctx, "sh", "-c", "mkdir -p ~/.config/coder && cat > ~/.config/coder/session") // !IMPORTANT: be careful that this does not appear in logs cmd.Stdin = strings.NewReader(creds.token) diff --git a/ci/tcli/doc.go b/ci/tcli/doc.go index f82d3df3..561dc480 100644 --- a/ci/tcli/doc.go +++ b/ci/tcli/doc.go @@ -1,4 +1,4 @@ // Package tcli provides a framework for CLI integration testing. -// Execute commands on the raw host of inside docker container. +// Execute commands on the raw host or inside a docker container. // Define custom Assertion types to extend test functionality. package tcli diff --git a/ci/tcli/tcli.go b/ci/tcli/tcli.go index de9fab86..7aa2f6fd 100644 --- a/ci/tcli/tcli.go +++ b/ci/tcli/tcli.go @@ -102,23 +102,40 @@ func (r *ContainerRunner) Close() error { return nil } +// Run executes the given command in the runtime container with reasonable defaults. +// "command" is executed in a shell as an argument to "sh -c". +func (r *ContainerRunner) Run(ctx context.Context, command string) *Assertable { + cmd := exec.CommandContext(ctx, + "docker", "exec", "-i", r.name, + "sh", "-c", command, + ) + + return &Assertable{ + cmd: cmd, + tname: command, + } +} + +// RunCmd lifts the given *exec.Cmd into the runtime container +func (r *ContainerRunner) RunCmd(cmd *exec.Cmd) *Assertable { + path, _ := exec.LookPath("docker") + cmd.Path = path + command := strings.Join(cmd.Args, " ") + cmd.Args = append([]string{"docker", "exec", "-i", r.name}, cmd.Args...) + + return &Assertable{ + cmd: cmd, + tname: command, + } +} + // HostRunner executes command tests on the host, outside of a container type HostRunner struct{} -// Run executes the given command on the host +// Run executes the given command on the host. +// "command" is executed in a shell as an argument to "sh -c". func (r *HostRunner) Run(ctx context.Context, command string) *Assertable { - var ( - args []string - path string - parts = strings.Split(command, " ") - ) - if len(parts) > 0 { - path = parts[0] - } - if len(parts) > 1 { - args = parts[1:] - } - cmd := exec.CommandContext(ctx, path, args...) + cmd := exec.CommandContext(ctx, "sh", "-c", command) return &Assertable{ cmd: cmd, @@ -126,7 +143,7 @@ func (r *HostRunner) Run(ctx context.Context, command string) *Assertable { } } -// RunCmd executes the given command on the host +// RunCmd executes the given *exec.Cmd on the host func (r *HostRunner) RunCmd(cmd *exec.Cmd) *Assertable { return &Assertable{ cmd: cmd, @@ -145,32 +162,6 @@ type Assertable struct { tname string } -// Run executes the given command in the runtime container with reasonable defaults -func (r *ContainerRunner) Run(ctx context.Context, command string) *Assertable { - cmd := exec.CommandContext(ctx, - "docker", "exec", "-i", r.name, - "sh", "-c", command, - ) - - return &Assertable{ - cmd: cmd, - tname: command, - } -} - -// RunCmd lifts the given *exec.Cmd into the runtime container -func (r *ContainerRunner) RunCmd(cmd *exec.Cmd) *Assertable { - path, _ := exec.LookPath("docker") - cmd.Path = path - command := strings.Join(cmd.Args, " ") - cmd.Args = []string{"docker", "exec", "-i", r.name, "sh", "-c", command} - - return &Assertable{ - cmd: cmd, - tname: command, - } -} - // Assert runs the Assertable and func (a Assertable) Assert(t *testing.T, option ...Assertion) { slog.Helper() From 692b219e64bba190f55c9c121072353e05fb8da6 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Wed, 29 Jul 2020 12:34:44 -0500 Subject: [PATCH 19/19] Handle command failure better --- ci/tcli/tcli.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ci/tcli/tcli.go b/ci/tcli/tcli.go index 7aa2f6fd..101cc926 100644 --- a/ci/tcli/tcli.go +++ b/ci/tcli/tcli.go @@ -181,8 +181,7 @@ func (a Assertable) Assert(t *testing.T, option ...Assertion) { if exitErr, ok := err.(*exec.ExitError); ok { result.ExitCode = exitErr.ExitCode() } else if err != nil { - // TODO: handle this case better - result.ExitCode = -1 + slogtest.Fatal(t, "command failed to run", slog.Error(err), slog.F("command", a.cmd)) } else { result.ExitCode = 0 } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy