Skip to content

Commit

Permalink
Add CLI tests (#21)
Browse files Browse the repository at this point in the history
* [wip] add CLI test foundation

Signed-off-by: Alex Goodman <[email protected]>

* test: wire CLI tests up in Taskfile

Signed-off-by: Will Murphy <[email protected]>

---------

Signed-off-by: Alex Goodman <[email protected]>
Signed-off-by: Will Murphy <[email protected]>
Co-authored-by: Alex Goodman <[email protected]>
  • Loading branch information
willmurphyscode and wagoodman authored May 9, 2024
1 parent 479001c commit 01de30c
Show file tree
Hide file tree
Showing 6 changed files with 430 additions and 1 deletion.
22 changes: 22 additions & 0 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ vars:

TMP_DIR: .tmp
SNAPSHOT_DIR: snapshot
# .ROOT_DIR is built-in variable, see https://taskfile.dev/api/#special-variables
SNAPSHOT_BIN: "{{ .ROOT_DIR }}/{{ .SNAPSHOT_DIR }}/{{ OS }}-build_{{ OS }}_{{ ARCH }}/{{ .PROJECT }}"
CHANGELOG: CHANGELOG.md
NEXT_VERSION: VERSION
MAKEDIR_P: 'python -c "import sys; import os; os.makedirs(sys.argv[1], exist_ok=True)"'
Expand Down Expand Up @@ -37,6 +39,7 @@ tasks:
desc: Run all levels of test
cmds:
- task: unit
- task: cli

## Bootstrap tasks #################################

Expand Down Expand Up @@ -134,6 +137,19 @@ tasks:
- cmd: '{{if eq OS "windows"}}python {{end}}.github/scripts/coverage.py {{ .COVERAGE_THRESHOLD }} {{ .TMP_DIR }}/unit-coverage-details.txt'
silent: true

cli:
desc: Run CLI tests
deps: [snapshot]
vars:
TEST_PKGS: "go list ./test/cli/..."
sources:
- "{{ .SNAPSHOT_BIN }}"
- ./test/cli/**
- ./**/*.go
cmds:
- cmd: "echo 'testing {{ .SNAPSHOT_BIN }}'"
- cmd: "go test ./test/cli/..."

## Build-related targets #################################

changelog:
Expand All @@ -151,6 +167,10 @@ tasks:
aliases:
- build
deps: [tools]
sources:
- "{{ .SNAPSHOT_BIN }}"
- ./test/cli/**
- ./**/*.go
cmds:
- silent: true
cmd: |
Expand Down Expand Up @@ -197,3 +217,5 @@ tasks:
- cmd: "cat CHANGELOG.md"
silent: true
- "{{ .TOOL_DIR }}/goreleaser release --clean --release-notes CHANGELOG.md"

# yaml-language-server: $schema=https://taskfile.dev/schema.json
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/Masterminds/semver/v3 v3.2.1
github.com/Masterminds/sprig/v3 v3.2.3
github.com/OneOfOne/xxhash v1.2.8
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/anchore/bubbly v0.0.0-20230919123500-747f4abea05f
github.com/anchore/clio v0.0.0-20230823172630-c42d666061af
github.com/anchore/fangs v0.0.0-20230818131516-2186b10924fe
Expand Down Expand Up @@ -47,7 +48,6 @@ require (
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect
github.com/RageCage64/multilinediff v0.2.0 // indirect
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
github.com/adrg/xdg v0.4.0 // indirect
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
Expand Down
68 changes: 68 additions & 0 deletions test/cli/install_cmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package cli

import (
"testing"
)

func TestInstallCmd(t *testing.T) {

type step struct {
name string
args []string
env map[string]string
assertions []traitAssertion
}

tests := []struct {
name string
steps []step
}{
{
name: "use go-install method",
steps: []step{
{
name: "install",
args: []string{"install", "-c", "testdata/go-install-method.yaml"},
assertions: []traitAssertion{
assertSuccessfulReturnCode,
assertFileInStoreExists(".binny.state.json"),
assertFileInStoreExists("binny"),
assertManagedToolOutput("binny", []string{"--version"}, "binny v0.7.0\n"),
},
},
{
name: "list",
args: []string{"list", "-c", "testdata/go-install-method.yaml", "-o", "json"},
assertions: []traitAssertion{
assertSuccessfulReturnCode,
assertJson,
assertInOutput(`"installedVersion": "v0.7.0"`),
},
},
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// we always have a clean slate for every test, but a shared state for each step
d := t.TempDir()

for _, s := range test.steps {
t.Run(s.name, func(t *testing.T) {
if s.env == nil {
s.env = make(map[string]string)
}
s.env["BINNY_ROOT"] = d

cmd, stdout, stderr := runBinny(t, s.env, s.args...)
for _, traitFn := range s.assertions {
traitFn(t, d, stdout, stderr, cmd.ProcessState.ExitCode())
}

logOutputOnFailure(t, cmd, stdout, stderr)
})
}
})
}
}
14 changes: 14 additions & 0 deletions test/cli/testdata/go-install-method.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
tools:
- name: binny
version:
want: v0.7.0
method: go-proxy
with:
module: github.com/anchore/binny
allow-unresolved-version: true
method: go-install
with:
entrypoint: cmd/binny
module: github.com/anchore/binny
ldflags:
- -X main.version={{ .Version }}
139 changes: 139 additions & 0 deletions test/cli/trait_assertions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package cli

import (
"encoding/json"
"github.com/google/go-cmp/cmp"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"testing"

"github.com/acarl005/stripansi"
"github.com/stretchr/testify/require"
)

type traitAssertion func(tb testing.TB, storeRoot, stdout, stderr string, rc int)

func assertFileOutput(tb testing.TB, path string, assertions ...traitAssertion) traitAssertion {
tb.Helper()

return func(tb testing.TB, storeRoot, _, stderr string, rc int) {
content, err := os.ReadFile(path)
require.NoError(tb, err)
contentStr := string(content)

for _, assertion := range assertions {
// treat the file content as stdout
assertion(tb, storeRoot, contentStr, stderr, rc)
}
}
}

func assertJson(tb testing.TB, _, stdout, _ string, _ int) {
tb.Helper()
var data interface{}

if err := json.Unmarshal([]byte(stdout), &data); err != nil {
tb.Errorf("expected to find a JSON report, but was unmarshalable: %+v", err)
}
}

func assertLoggingLevel(level string) traitAssertion {
// match examples:
// "[0000] INFO"
// "[0012] DEBUG"
logPattern := regexp.MustCompile(`(?m)^\[\d\d\d\d\]\s+` + strings.ToUpper(level))
return func(tb testing.TB, _, _, stderr string, _ int) {
tb.Helper()
if !logPattern.MatchString(stripansi.Strip(stderr)) {
tb.Errorf("output did not indicate the %q logging level", level)
}
}
}

func assertNotInOutput(data string) traitAssertion {
return func(tb testing.TB, _, stdout, stderr string, _ int) {
tb.Helper()
if strings.Contains(stripansi.Strip(stderr), data) {
tb.Errorf("data=%q was found in stderr, but should not have been there", data)
}
if strings.Contains(stripansi.Strip(stdout), data) {
tb.Errorf("data=%q was found in stdout, but should not have been there", data)
}
}
}

func assertNoStderr(tb testing.TB, _, _, stderr string, _ int) {
tb.Helper()
if len(stderr) > 0 {
tb.Errorf("expected stderr to be empty, but wasn't")
if showOutput != nil && *showOutput {
tb.Errorf("STDERR:%s", stderr)
}
}
}

func assertInOutput(data string) traitAssertion {
return func(tb testing.TB, _, stdout, stderr string, _ int) {
tb.Helper()
stdout = stripansi.Strip(stdout)
stderr = stripansi.Strip(stderr)
if !strings.Contains(stdout, data) && !strings.Contains(stderr, data) {
tb.Errorf("data=%q was NOT found in any output, but should have been there", data)
if showOutput != nil && *showOutput {
tb.Errorf("STDOUT:%s\nSTDERR:%s", stdout, stderr)
}
}
}
}

func assertStdoutLengthGreaterThan(length uint) traitAssertion {
return func(tb testing.TB, _, stdout, _ string, _ int) {
tb.Helper()
if uint(len(stdout)) < length {
tb.Errorf("not enough output (expected at least %d, got %d)", length, len(stdout))
}
}
}

func assertFailingReturnCode(tb testing.TB, _, _, _ string, rc int) {
tb.Helper()
if rc == 0 {
tb.Errorf("expected a failure but got rc=%d", rc)
}
}

func assertSuccessfulReturnCode(tb testing.TB, _, _, _ string, rc int) {
tb.Helper()
if rc != 0 {
tb.Errorf("expected no failure but got rc=%d", rc)
}
}

func assertFileInStoreExists(file string) traitAssertion {
return func(tb testing.TB, storeRoot, _, _ string, _ int) {
tb.Helper()
path := filepath.Join(storeRoot, file)
if _, err := os.Stat(path); err != nil {
tb.Errorf("expected file to exist %s", path)
}
}
}

func assertManagedToolOutput(tool string, args []string, expectedStdout string) traitAssertion {
return func(tb testing.TB, storeRoot, _, _ string, _ int) {
tb.Helper()

path := filepath.Join(storeRoot, tool)
cmd := exec.Command(path, args...)

gotStdout, _, err := runCommand(cmd, nil)
require.NoError(tb, err)

if d := cmp.Diff(expectedStdout, gotStdout); d != "" {
tb.Errorf("unexpected output (-want +got):\n%s", d)
}
}
}
Loading

0 comments on commit 01de30c

Please sign in to comment.