Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding testing for survey prompts #51

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 5 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ go 1.17

require (
github.com/AlecAivazis/survey/v2 v2.3.4
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/cppforlife/go-cli-ui v0.0.0-20200716203538-1e47f820817f
github.com/creack/pty v1.1.18
github.com/fatih/color v1.13.0
github.com/go-logr/logr v1.2.3
github.com/google/go-cmp v0.5.7
github.com/google/go-containerregistry v0.8.0
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02
github.com/k14s/imgpkg v0.17.0
github.com/spf13/cobra v1.4.0
github.com/stern/stern v1.21.0
Expand Down Expand Up @@ -125,7 +128,7 @@ require (
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
Expand Down Expand Up @@ -169,7 +172,7 @@ require (
golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20220307203707-22a9840ba4d7 // indirect
golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
Expand Down
12 changes: 8 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -512,8 +512,9 @@ github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=
github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s=
Expand Down Expand Up @@ -1060,8 +1061,9 @@ github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKEN
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 h1:AgcIVYPa6XJnU3phs104wLj8l5GEththEw6+F79YsIY=
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hpcloud/tail v1.0.1-0.20180514194441-a1dbeea552b7/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
Expand Down Expand Up @@ -1241,8 +1243,9 @@ github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182aff
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY=
github.com/maxbrunsfeld/counterfeiter/v6 v6.4.1/go.mod h1:DK1Cjkc0E49ShgRVs5jy5ASrM15svSnem3K/hiSGD8o=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.17/go.mod h1:WgzbA6oji13JREwiNsRDNfl7jYdPnmz+VEuLrA+/48M=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
Expand Down Expand Up @@ -2159,8 +2162,9 @@ golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220307203707-22a9840ba4d7 h1:8IVLkfbr2cLhv0a/vKq4UFUcJym8RmDoDboxCFWEjYE=
golang.org/x/sys v0.0.0-20220307203707-22a9840ba4d7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f h1:rlezHXNlxYWvBCzNses9Dlc7nGFaNMJeqLolcmQSSZY=
golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
Expand Down
82 changes: 82 additions & 0 deletions pkg/cli-runtime/testing/command_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,20 @@ package testing
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"path"
"reflect"
"strings"
"testing"
"time"

"github.com/Netflix/go-expect"
pseudotty "github.com/creack/pty"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hinshun/vt10x"
"github.com/spf13/cobra"
rtesting "github.com/vmware-labs/reconciler-runtime/testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -161,6 +166,8 @@ type CommandTestCase struct {
// It is indended to clean up any state created in the Prepare step or during the test
// execution, or to make assertions for mocks.
CleanUp func(t *testing.T, ctx context.Context, config *cli.Config, tc *CommandTestCase) error
// WithConsoleInteractions receives function with an expect.Console that can be used to send characters and verify the output send to a fake console.
WithConsoleInteractions func(t *testing.T, console *expect.Console)
}

// Run each record for the table. Tables with a focused record will run only the focused records
Expand Down Expand Up @@ -260,8 +267,52 @@ func (tc CommandTestCase) Run(t *testing.T, scheme *runtime.Scheme, cmdFactory f
}
}()
}

var console *expect.Console
var donec chan struct{}
var term vt10x.Terminal
if tc.WithConsoleInteractions != nil {
var err error
// Setup stdio for prompt testing, multiplex output to a buffer as well for the raw bytes.
pty, tty, err := pseudotty.Open()
if err != nil {
t.Fatalf("failed to open pseudotty: %v", err)
}
term = vt10x.New(vt10x.WithWriter(tty), vt10x.WithSize(160, 24))
if err != nil {
t.Errorf("Unexpected error %s", err)
}

console, err := expect.NewConsole(expect.WithStdin(pty), expect.WithStdout(term), expect.WithCloser(pty, tty), expectNoError(t), expect.WithDefaultTimeout(3*time.Second))
if err != nil {
t.Fatalf("failed to create console: %v", err)
}

defer console.Close()

donec = make(chan struct{})
go func() {
defer close(donec)
tc.WithConsoleInteractions(t, console)
}()

c.Stdout = console.Tty()
c.Stderr = console.Tty()
c.Stdin = console.Tty()
}

cmdErr := cmd.Execute()

// Close the slave end of the pty
if console != nil {
console.Tty().Close()
}

// Read the remaining bytes from the master end if needed
if donec != nil {
<-donec
}

if expected, actual := tc.ShouldError, cmdErr != nil; expected != actual {
if expected {
t.Errorf("expected command to error, actual %v", cmdErr)
Expand Down Expand Up @@ -312,6 +363,10 @@ func (tc CommandTestCase) Run(t *testing.T, scheme *runtime.Scheme, cmdFactory f
}

outputString := output.String()
// terminalOutput doesn't contain ANSI escape sequences, we compare that when using an interactive terminal
if term != nil {
outputString = trimTrailingSpaces(expect.StripTrailingEmptyLines(term.String()))
}
if tc.ExpectOutput != "" {
if diff := cmp.Diff(strings.TrimPrefix(tc.ExpectOutput, "\n"), outputString); diff != "" {
t.Errorf("Unexpected output (-expected, +actual): %s", diff)
Expand All @@ -329,13 +384,40 @@ func (tc CommandTestCase) Run(t *testing.T, scheme *runtime.Scheme, cmdFactory f
})
}

func trimTrailingSpaces(in string) string {
lines := strings.Split(in, "\n")
for i := len(lines) - 1; i >= 0; i-- {
lines[i] = strings.TrimRight(lines[i], " ")
}
return strings.Join(lines, "\n")
}

func objKey(o runtime.Object) string {
on := o.(metav1.ObjectMetaAccessor)
// namespace + name is not unique, and the tests don't populate k8s kind
// information, so use GoLang's type name as part of the key.
return path.Join(reflect.TypeOf(o).String(), on.GetObjectMeta().GetNamespace(), on.GetObjectMeta().GetName())
}

func expectNoError(t *testing.T) expect.ConsoleOpt {
return expect.WithExpectObserver(
func(matchers []expect.Matcher, buf string, err error) {
if err == nil {
return
}
if len(matchers) == 0 {
t.Fatalf("Error occurred while matching %q: %s\n", buf, err)
} else {
var criteria []string
for _, matcher := range matchers {
criteria = append(criteria, fmt.Sprintf("%q", matcher.Criteria()))
}
t.Fatalf("Unexpected output; expected: %s ; got %q: \nError: %s\n", strings.Join(criteria, ", "), buf, err)
}
},
)
}

type DeleteRef struct {
Group string
Resource string
Expand Down
163 changes: 163 additions & 0 deletions pkg/commands/workload_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"testing"
"time"

"github.com/Netflix/go-expect"
"github.com/spf13/cobra"
"github.com/stretchr/testify/mock"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -1580,6 +1581,168 @@ Updated workload "my-workload"
return nil
},
},
// Test prompts
{
Name: "Apply with existing workload and confirm prompt",
Args: []string{workloadName, flags.GitRepoFlagName, gitRepo, flags.GitBranchFlagName, gitBranch},
GivenObjects: []clitesting.Factory{
clitesting.Wrapper(&cartov1alpha1.Workload{
ObjectMeta: metav1.ObjectMeta{
Namespace: defaultNamespace,
Name: workloadName,
},
Spec: cartov1alpha1.WorkloadSpec{
Image: "ubuntu:bionic",
},
}),
},
ExpectUpdates: []client.Object{
&cartov1alpha1.Workload{
ObjectMeta: metav1.ObjectMeta{
Namespace: defaultNamespace,
Name: workloadName,
},
Spec: cartov1alpha1.WorkloadSpec{
Source: &cartov1alpha1.Source{
Git: &cartov1alpha1.GitSource{
URL: gitRepo,
Ref: cartov1alpha1.GitRef{
Branch: gitBranch,
},
},
},
},
},
},
WithConsoleInteractions: func(t *testing.T, c *expect.Console) {
c.Expectf("Really update the workload %q?", workloadName)
c.SendLine("y")
c.Expectf("Updated workload %q", workloadName)
},
ExpectOutput: `
Update workload:
...
4, 4 |metadata:
5, 5 | name: my-workload
6, 6 | namespace: default
7, 7 |spec:
8 - | image: ubuntu:bionic
8 + | source:
9 + | git:
10 + | ref:
11 + | branch: main
12 + | url: https://example.com/repo.git

? Really update the workload "my-workload"? Yes
Updated workload "my-workload"`,
},
{
Name: "Apply with existing workload and reject prompt",
Args: []string{workloadName, flags.GitRepoFlagName, gitRepo, flags.GitBranchFlagName, gitBranch},
GivenObjects: []clitesting.Factory{
clitesting.Wrapper(&cartov1alpha1.Workload{
ObjectMeta: metav1.ObjectMeta{
Namespace: defaultNamespace,
Name: workloadName,
},
Spec: cartov1alpha1.WorkloadSpec{
Image: "ubuntu:bionic",
},
}),
},
WithConsoleInteractions: func(t *testing.T, c *expect.Console) {
c.Expectf("Really update the workload %q?", workloadName)
c.SendLine("n")
c.Expectf("Skipping workload %q", workloadName)
},
ExpectOutput: `
Update workload:
...
4, 4 |metadata:
5, 5 | name: my-workload
6, 6 | namespace: default
7, 7 |spec:
8 - | image: ubuntu:bionic
8 + | source:
9 + | git:
10 + | ref:
11 + | branch: main
12 + | url: https://example.com/repo.git

? Really update the workload "my-workload"? No
Skipping workload "my-workload"`,
},
{
Name: "Apply with new workload and confirm prompt",
Args: []string{workloadName, flags.GitRepoFlagName, gitRepo, flags.GitBranchFlagName, gitBranch},
ExpectCreates: []client.Object{
&cartov1alpha1.Workload{
ObjectMeta: metav1.ObjectMeta{
Namespace: defaultNamespace,
Name: workloadName,
Labels: map[string]string{},
},
Spec: cartov1alpha1.WorkloadSpec{
Source: &cartov1alpha1.Source{
Git: &cartov1alpha1.GitSource{
URL: gitRepo,
Ref: cartov1alpha1.GitRef{
Branch: gitBranch,
},
},
},
},
},
},
WithConsoleInteractions: func(t *testing.T, c *expect.Console) {
c.ExpectString("Do you want to create this workload?")
c.SendLine("y")
c.Expectf("Created workload %q", workloadName)
},
ExpectOutput: `
Create workload:
1 + |---
2 + |apiVersion: carto.run/v1alpha1
3 + |kind: Workload
4 + |metadata:
5 + | name: my-workload
6 + | namespace: default
7 + |spec:
8 + | source:
9 + | git:
10 + | ref:
11 + | branch: main
12 + | url: https://example.com/repo.git

? Do you want to create this workload? Yes
Created workload "my-workload"`,
},
{
Name: "Apply with new workload and reject prompt",
Args: []string{workloadName, flags.GitRepoFlagName, gitRepo, flags.GitBranchFlagName, gitBranch},
WithConsoleInteractions: func(t *testing.T, c *expect.Console) {
c.ExpectString("Do you want to create this workload?")
c.SendLine("n")
c.Expectf("Skipping workload %q", workloadName)
},
ExpectOutput: `
Create workload:
1 + |---
2 + |apiVersion: carto.run/v1alpha1
3 + |kind: Workload
4 + |metadata:
5 + | name: my-workload
6 + | namespace: default
7 + |spec:
8 + | source:
9 + | git:
10 + | ref:
11 + | branch: main
12 + | url: https://example.com/repo.git

? Do you want to create this workload? No
Skipping workload "my-workload"`,
},
{
Name: "filepath invalid name",
Args: []string{flags.FilePathFlagName, "testdata/workload-invalid-name.yaml", flags.YesFlagName},
Expand Down