From aba7dfcafbb38dee5883477da30eb5671eafe0c7 Mon Sep 17 00:00:00 2001 From: PuneetPunamiya Date: Thu, 25 Apr 2024 20:14:54 +0530 Subject: [PATCH] Bootstrap and adds e2e tests Signed-off-by: PuneetPunamiya --- test/cli/cli.go | 42 ++++ test/cli_e2e_test.go | 74 ++++++ test/e2e_test.go | 1 - test/resources/create.go | 135 +++++++++++ test/resources/customrun.go | 8 +- test/testdata/cr-1.yaml | 18 ++ test/testdata/cr-2.yaml | 18 ++ test/testdata/cr-3.yaml | 18 ++ vendor/gotest.tools/v3/icmd/command.go | 291 ++++++++++++++++++++++++ vendor/gotest.tools/v3/icmd/exitcode.go | 32 +++ vendor/gotest.tools/v3/icmd/ops.go | 46 ++++ vendor/modules.txt | 1 + 12 files changed, 679 insertions(+), 5 deletions(-) create mode 100644 test/cli/cli.go create mode 100644 test/cli_e2e_test.go create mode 100644 test/resources/create.go create mode 100644 test/testdata/cr-1.yaml create mode 100644 test/testdata/cr-2.yaml create mode 100644 test/testdata/cr-3.yaml create mode 100644 vendor/gotest.tools/v3/icmd/command.go create mode 100644 vendor/gotest.tools/v3/icmd/exitcode.go create mode 100644 vendor/gotest.tools/v3/icmd/ops.go diff --git a/test/cli/cli.go b/test/cli/cli.go new file mode 100644 index 00000000..4c5a1cb1 --- /dev/null +++ b/test/cli/cli.go @@ -0,0 +1,42 @@ +package cli + +import ( + "fmt" + "os" + "testing" + + "gotest.tools/v3/icmd" +) + +type TknApprovalTaskRunner struct { + path string + namespace string +} + +func NewTknApprovalTaskRunner() (TknApprovalTaskRunner, error) { + if os.Getenv("TEST_CLIENT_BINARY") != "" { + return TknApprovalTaskRunner{ + path: os.Getenv("TEST_CLIENT_BINARY"), + }, nil + } + return TknApprovalTaskRunner{ + path: os.Getenv("TEST_CLIENT_BINARY"), + }, fmt.Errorf("Error: couldn't Create tknApprovalTaskRunner, please do check tkn binary path: (%+v)", os.Getenv("TEST_CLIENT_BINARY")) +} + +func (tknApprovalTaskRunner TknApprovalTaskRunner) Run(args ...string) *icmd.Result { + cmd := append([]string{tknApprovalTaskRunner.path}, args...) + return icmd.RunCmd(icmd.Cmd{Command: cmd}) +} + +// MustSucceed asserts that the command ran with 0 exit code +func (tknApprovalTaskRunner TknApprovalTaskRunner) MustSucceed(t *testing.T, args ...string) *icmd.Result { + return tknApprovalTaskRunner.Assert(t, icmd.Success, args...) +} + +// Assert runs a command and verifies exit code (0) +func (tknApprovalTaskRunner TknApprovalTaskRunner) Assert(t *testing.T, exp icmd.Expected, args ...string) *icmd.Result { + res := tknApprovalTaskRunner.Run(args...) + res.Assert(t, exp) + return res +} diff --git a/test/cli_e2e_test.go b/test/cli_e2e_test.go new file mode 100644 index 00000000..a86dbcbe --- /dev/null +++ b/test/cli_e2e_test.go @@ -0,0 +1,74 @@ +package test + +import ( + "testing" + + "github.com/openshift-pipelines/manual-approval-gate/test/cli" + "github.com/openshift-pipelines/manual-approval-gate/test/resources" + "github.com/stretchr/testify/assert" +) + +var expectedAt = `NAME NumberOfApprovalsRequired PendingApprovals Rejected STATUS +cr-1 2 0 1 Rejected +cr-2 2 2 0 Pending +cr-3 2 0 0 Approved +` + +func TestApprovalTaskList(t *testing.T) { + tknApprovaltask, err := cli.NewTknApprovalTaskRunner() + assert.Nil(t, err) + + t.Run("No approvaltask found", func(t *testing.T) { + expected := "No ApprovalTasks found\n" + + res := tknApprovaltask.MustSucceed(t, "list", "-n", "foo") + assert.Equal(t, expected, res.Stdout()) + }) + + t.Run("List approvaltask in `test-1` namespace", func(t *testing.T) { + clients, cr := resources.Create(t, "./testdata/cr-1.yaml") + + approvers := []resources.Approver{ + { + Name: "foo", + Input: "approve", + }, + { + Name: "tekton", + Input: "reject", + }, + } + resources.Update(t, clients, cr, approvers) + + _, _ = resources.Create(t, "./testdata/cr-2.yaml") + + clients, cr3 := resources.Create(t, "./testdata/cr-3.yaml") + + approvers3 := []resources.Approver{ + { + Name: "foo", + Input: "approve", + }, + { + Name: "bar", + Input: "approve", + }, + } + resources.Update(t, clients, cr3, approvers3) + + // approvers := []resources.Approver{ + // { + // Name: "foo", + // Input: "approve", + // }, + // { + // Name: "tekton", + // Input: "reject", + // }, + // } + // resources.Update(t, clients, cr2, []resources.Approver{}) + + res := tknApprovaltask.MustSucceed(t, "list", "-n", "test-1") + assert.Equal(t, expectedAt, res.Stdout()) + }) +} diff --git a/test/e2e_test.go b/test/e2e_test.go index 3906705e..5a06f245 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -55,7 +55,6 @@ func TestApproveManualApprovalTask(t *testing.T) { } }) - // Test if TektonConfig can reach the READY status t.Run("ensure-approval-task-creation", func(t *testing.T) { _, err := resources.WaitForApprovalTaskCreation(clients.ApprovalTaskClient, cr.GetName()) if err != nil { diff --git a/test/resources/create.go b/test/resources/create.go new file mode 100644 index 00000000..4e725af3 --- /dev/null +++ b/test/resources/create.go @@ -0,0 +1,135 @@ +package resources + +import ( + "context" + "io/ioutil" + "path/filepath" + "testing" + + "github.com/openshift-pipelines/manual-approval-gate/pkg/apis/approvaltask/v1alpha1" + "github.com/openshift-pipelines/manual-approval-gate/test/client" + "github.com/openshift-pipelines/manual-approval-gate/test/utils" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" +) + +func Create(t *testing.T, path string) (*utils.Clients, *v1beta1.CustomRun) { + clients := client.Setup(t, "default") + + taskRunPath, err := filepath.Abs(path) + if err != nil { + t.Fatal(err) + } + + taskRunYAML, err := ioutil.ReadFile(taskRunPath) + if err != nil { + t.Fatal(err) + } + + customRun := MustParseCustomRun(t, string(taskRunYAML)) + + var cr *v1beta1.CustomRun + t.Run("ensure-custom-run-creation", func(t *testing.T) { + cr, err = EnsureCustomTaskRunExists(clients.TektonClient, customRun) + if err != nil { + t.Fatalf("Failed to create the custom run: %v", err) + } + }) + + t.Run("ensure-approval-task-creation", func(t *testing.T) { + _, err := WaitForApprovalTaskCreation(clients.ApprovalTaskClient, cr.GetName(), cr.GetNamespace()) + if err != nil { + t.Fatal("Failed to get the approval task") + } + }) + + // t.Run("update the approval task", func(t *testing.T) { + // at, err := clients.ApprovalTaskClient.ApprovalTasks(cr.GetNamespace()).Get(context.TODO(), cr.GetName(), metav1.GetOptions{}) + // if err != nil { + // t.Fatal("Failed to get the approvaltask") + // } + + // att := updateApprovalTask(at, "foo", "reject") + + // _, err = clients.ApprovalTaskClient.ApprovalTasks(att.Namespace).Update(context.TODO(), att, metav1.UpdateOptions{}) + // if err != nil { + // t.Fatal("Failed to update the approvalTask..") + // } + + // _, err = WaitForApprovalTaskStatusUpdate(clients.ApprovalTaskClient, cr.GetName(), cr.GetNamespace(), "rejected") + // if err != nil { + // t.Fatal("Failed to get the approval task") + // } + + // approvalTask, err := clients.ApprovalTaskClient.ApprovalTasks(cr.GetNamespace()).Get(context.TODO(), cr.GetName(), metav1.GetOptions{}) + // if err != nil { + // t.Fatal("Failed to get the approval task") + // } + // assert.Equal(t, "rejected", approvalTask.Status.State) + // }) + + return clients, cr +} + +type Approver struct { + Name string + Input string +} + +func Update(t *testing.T, clients *utils.Clients, cr *v1beta1.CustomRun, approvers []Approver) { + t.Run("update the approval task", func(t *testing.T) { + at, err := clients.ApprovalTaskClient.ApprovalTasks(cr.GetNamespace()).Get(context.TODO(), cr.GetName(), metav1.GetOptions{}) + if err != nil { + t.Fatal("Failed to get the approvaltask") + } + + att := updateApprovalTask(at, approvers) + + _, err = clients.ApprovalTaskClient.ApprovalTasks(att.Namespace).Update(context.TODO(), att, metav1.UpdateOptions{}) + if err != nil { + t.Fatal("Failed to update the approvalTask..") + } + + // _, err = WaitForApprovalTaskStatusUpdate(clients.ApprovalTaskClient, cr.GetName(), cr.GetNamespace(), "rejected") + // if err != nil { + // t.Fatal("Failed to get the approval task") + // } + + // approvalTask, err := clients.ApprovalTaskClient.ApprovalTasks(cr.GetNamespace()).Get(context.TODO(), cr.GetName(), metav1.GetOptions{}) + // if err != nil { + // t.Fatal("Failed to get the approval task") + // } + // assert.Equal(t, "rejected", approvalTask.Status.State) + }) +} + +func updateApprovalTask(at *v1alpha1.ApprovalTask, approvers []Approver) *v1alpha1.ApprovalTask { + for _, approver := range approvers { + for j, a := range at.Spec.Approvers { + if a.Name == approver.Name { + at.Spec.Approvers[j].Input = approver.Input + } + } + } + + return at +} + +func MustParseCustomRun(t *testing.T, yaml string) *v1beta1.CustomRun { + t.Helper() + var r v1beta1.CustomRun + yaml = `apiVersion: tekton.dev/v1beta1 +kind: CustomRun +` + yaml + mustParseYAML(t, yaml, &r) + return &r +} + +func mustParseYAML(t *testing.T, yaml string, i runtime.Object) { + t.Helper() + if _, _, err := scheme.Codecs.UniversalDeserializer().Decode([]byte(yaml), nil, i); err != nil { + t.Fatalf("mustParseYAML (%s): %v", yaml, err) + } +} diff --git a/test/resources/customrun.go b/test/resources/customrun.go index 6b934051..a6f9f2d5 100644 --- a/test/resources/customrun.go +++ b/test/resources/customrun.go @@ -32,10 +32,10 @@ func EnsureCustomTaskRunExists(client pipelinev1beta1.TektonV1beta1Interface, cu return cr, err } -func WaitForApprovalTaskCreation(client typedopenshiftpipelinesv1alpha1.OpenshiftpipelinesV1alpha1Interface, name string) (*v1alpha1.ApprovalTask, error) { +func WaitForApprovalTaskCreation(client typedopenshiftpipelinesv1alpha1.OpenshiftpipelinesV1alpha1Interface, name, namespace string) (*v1alpha1.ApprovalTask, error) { var lastState *v1alpha1.ApprovalTask waitErr := wait.PollImmediate(Interval, Timeout, func() (done bool, err error) { - _, err = client.ApprovalTasks("default").Get(context.TODO(), name, metav1.GetOptions{}) + _, err = client.ApprovalTasks("test-1").Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return false, nil } @@ -49,11 +49,11 @@ func WaitForApprovalTaskCreation(client typedopenshiftpipelinesv1alpha1.Openshif return lastState, nil } -func WaitForApprovalTaskStatusUpdate(client typedopenshiftpipelinesv1alpha1.OpenshiftpipelinesV1alpha1Interface, name, desiredStatus string) (*v1alpha1.ApprovalTask, error) { +func WaitForApprovalTaskStatusUpdate(client typedopenshiftpipelinesv1alpha1.OpenshiftpipelinesV1alpha1Interface, name, namespace, desiredStatus string) (*v1alpha1.ApprovalTask, error) { var approvalTask *v1alpha1.ApprovalTask waitErr := wait.PollImmediate(Interval, Timeout, func() (done bool, err error) { - approvalTask, err = client.ApprovalTasks("default").Get(context.TODO(), name, metav1.GetOptions{}) + approvalTask, err = client.ApprovalTasks("test-1").Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return false, err } diff --git a/test/testdata/cr-1.yaml b/test/testdata/cr-1.yaml new file mode 100644 index 00000000..6b9baf09 --- /dev/null +++ b/test/testdata/cr-1.yaml @@ -0,0 +1,18 @@ +apiVersion: tekton.dev/v1beta1 +kind: CustomRun +metadata: + name: cr-1 + namespace: test-1 +spec: + retries: 2 + customRef: + apiVersion: openshift-pipelines.org/v1alpha1 + kind: ApprovalTask + params: + - name: approvers + value: + - foo + - bar + - tekton + - name: numberOfApprovalsRequired + value: 2 \ No newline at end of file diff --git a/test/testdata/cr-2.yaml b/test/testdata/cr-2.yaml new file mode 100644 index 00000000..912b1061 --- /dev/null +++ b/test/testdata/cr-2.yaml @@ -0,0 +1,18 @@ +apiVersion: tekton.dev/v1beta1 +kind: CustomRun +metadata: + name: cr-2 + namespace: test-1 +spec: + retries: 2 + customRef: + apiVersion: openshift-pipelines.org/v1alpha1 + kind: ApprovalTask + params: + - name: approvers + value: + - foo + - bar + - tekton + - name: numberOfApprovalsRequired + value: 2 \ No newline at end of file diff --git a/test/testdata/cr-3.yaml b/test/testdata/cr-3.yaml new file mode 100644 index 00000000..e9af0356 --- /dev/null +++ b/test/testdata/cr-3.yaml @@ -0,0 +1,18 @@ +apiVersion: tekton.dev/v1beta1 +kind: CustomRun +metadata: + name: cr-3 + namespace: test-1 +spec: + retries: 2 + customRef: + apiVersion: openshift-pipelines.org/v1alpha1 + kind: ApprovalTask + params: + - name: approvers + value: + - foo + - bar + - tekton + - name: numberOfApprovalsRequired + value: 2 \ No newline at end of file diff --git a/vendor/gotest.tools/v3/icmd/command.go b/vendor/gotest.tools/v3/icmd/command.go new file mode 100644 index 00000000..822ee94b --- /dev/null +++ b/vendor/gotest.tools/v3/icmd/command.go @@ -0,0 +1,291 @@ +/*Package icmd executes binaries and provides convenient assertions for testing the results. + */ +package icmd // import "gotest.tools/v3/icmd" + +import ( + "bytes" + "fmt" + "io" + "os" + "strings" + "sync" + "time" + + exec "golang.org/x/sys/execabs" + "gotest.tools/v3/assert" + "gotest.tools/v3/assert/cmp" +) + +type helperT interface { + Helper() +} + +// None is a token to inform Result.Assert that the output should be empty +const None = "[NOTHING]" + +type lockedBuffer struct { + m sync.RWMutex + buf bytes.Buffer +} + +func (buf *lockedBuffer) Write(b []byte) (int, error) { + buf.m.Lock() + defer buf.m.Unlock() + return buf.buf.Write(b) +} + +func (buf *lockedBuffer) String() string { + buf.m.RLock() + defer buf.m.RUnlock() + return buf.buf.String() +} + +// Result stores the result of running a command +type Result struct { + Cmd *exec.Cmd + ExitCode int + Error error + // Timeout is true if the command was killed because it ran for too long + Timeout bool + outBuffer *lockedBuffer + errBuffer *lockedBuffer +} + +// Assert compares the Result against the Expected struct, and fails the test if +// any of the expectations are not met. +// +// This function is equivalent to assert.Assert(t, result.Equal(exp)). +func (r *Result) Assert(t assert.TestingT, exp Expected) *Result { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + assert.Assert(t, r.Equal(exp)) + return r +} + +// Equal compares the result to Expected. If the result doesn't match expected +// returns a formatted failure message with the command, stdout, stderr, exit code, +// and any failed expectations. +func (r *Result) Equal(exp Expected) cmp.Comparison { + return func() cmp.Result { + return cmp.ResultFromError(r.match(exp)) + } +} + +// Compare the result to Expected and return an error if they do not match. +func (r *Result) Compare(exp Expected) error { + return r.match(exp) +} + +// nolint: gocyclo +func (r *Result) match(exp Expected) error { + errors := []string{} + add := func(format string, args ...interface{}) { + errors = append(errors, fmt.Sprintf(format, args...)) + } + + if exp.ExitCode != r.ExitCode { + add("ExitCode was %d expected %d", r.ExitCode, exp.ExitCode) + } + if exp.Timeout != r.Timeout { + if exp.Timeout { + add("Expected command to timeout") + } else { + add("Expected command to finish, but it hit the timeout") + } + } + if !matchOutput(exp.Out, r.Stdout()) { + add("Expected stdout to contain %q", exp.Out) + } + if !matchOutput(exp.Err, r.Stderr()) { + add("Expected stderr to contain %q", exp.Err) + } + switch { + // If a non-zero exit code is expected there is going to be an error. + // Don't require an error message as well as an exit code because the + // error message is going to be "exit status which is not useful + case exp.Error == "" && exp.ExitCode != 0: + case exp.Error == "" && r.Error != nil: + add("Expected no error") + case exp.Error != "" && r.Error == nil: + add("Expected error to contain %q, but there was no error", exp.Error) + case exp.Error != "" && !strings.Contains(r.Error.Error(), exp.Error): + add("Expected error to contain %q", exp.Error) + } + + if len(errors) == 0 { + return nil + } + return fmt.Errorf("%s\nFailures:\n%s", r, strings.Join(errors, "\n")) +} + +func matchOutput(expected string, actual string) bool { + switch expected { + case None: + return actual == "" + default: + return strings.Contains(actual, expected) + } +} + +func (r *Result) String() string { + var timeout string + if r.Timeout { + timeout = " (timeout)" + } + var errString string + if r.Error != nil { + errString = "\nError: " + r.Error.Error() + } + + return fmt.Sprintf(` +Command: %s +ExitCode: %d%s%s +Stdout: %v +Stderr: %v +`, + strings.Join(r.Cmd.Args, " "), + r.ExitCode, + timeout, + errString, + r.Stdout(), + r.Stderr()) +} + +// Expected is the expected output from a Command. This struct is compared to a +// Result struct by Result.Assert(). +type Expected struct { + ExitCode int + Timeout bool + Error string + Out string + Err string +} + +// Success is the default expected result. A Success result is one with a 0 +// ExitCode. +var Success = Expected{} + +// Stdout returns the stdout of the process as a string +func (r *Result) Stdout() string { + return r.outBuffer.String() +} + +// Stderr returns the stderr of the process as a string +func (r *Result) Stderr() string { + return r.errBuffer.String() +} + +// Combined returns the stdout and stderr combined into a single string +func (r *Result) Combined() string { + return r.outBuffer.String() + r.errBuffer.String() +} + +func (r *Result) setExitError(err error) { + if err == nil { + return + } + r.Error = err + r.ExitCode = processExitCode(err) +} + +// Cmd contains the arguments and options for a process to run as part of a test +// suite. +type Cmd struct { + Command []string + Timeout time.Duration + Stdin io.Reader + Stdout io.Writer + Dir string + Env []string + ExtraFiles []*os.File +} + +// Command create a simple Cmd with the specified command and arguments +func Command(command string, args ...string) Cmd { + return Cmd{Command: append([]string{command}, args...)} +} + +// RunCmd runs a command and returns a Result +func RunCmd(cmd Cmd, cmdOperators ...CmdOp) *Result { + for _, op := range cmdOperators { + op(&cmd) + } + result := StartCmd(cmd) + if result.Error != nil { + return result + } + return WaitOnCmd(cmd.Timeout, result) +} + +// RunCommand runs a command with default options, and returns a result +func RunCommand(command string, args ...string) *Result { + return RunCmd(Command(command, args...)) +} + +// StartCmd starts a command, but doesn't wait for it to finish +func StartCmd(cmd Cmd) *Result { + result := buildCmd(cmd) + if result.Error != nil { + return result + } + result.setExitError(result.Cmd.Start()) + return result +} + +// TODO: support exec.CommandContext +func buildCmd(cmd Cmd) *Result { + var execCmd *exec.Cmd + switch len(cmd.Command) { + case 1: + execCmd = exec.Command(cmd.Command[0]) + default: + execCmd = exec.Command(cmd.Command[0], cmd.Command[1:]...) + } + outBuffer := new(lockedBuffer) + errBuffer := new(lockedBuffer) + + execCmd.Stdin = cmd.Stdin + execCmd.Dir = cmd.Dir + execCmd.Env = cmd.Env + if cmd.Stdout != nil { + execCmd.Stdout = io.MultiWriter(outBuffer, cmd.Stdout) + } else { + execCmd.Stdout = outBuffer + } + execCmd.Stderr = errBuffer + execCmd.ExtraFiles = cmd.ExtraFiles + + return &Result{ + Cmd: execCmd, + outBuffer: outBuffer, + errBuffer: errBuffer, + } +} + +// WaitOnCmd waits for a command to complete. If timeout is non-nil then +// only wait until the timeout. +func WaitOnCmd(timeout time.Duration, result *Result) *Result { + if timeout == time.Duration(0) { + result.setExitError(result.Cmd.Wait()) + return result + } + + done := make(chan error, 1) + // Wait for command to exit in a goroutine + go func() { + done <- result.Cmd.Wait() + }() + + select { + case <-time.After(timeout): + killErr := result.Cmd.Process.Kill() + if killErr != nil { + fmt.Printf("failed to kill (pid=%d): %v\n", result.Cmd.Process.Pid, killErr) + } + result.Timeout = true + case err := <-done: + result.setExitError(err) + } + return result +} diff --git a/vendor/gotest.tools/v3/icmd/exitcode.go b/vendor/gotest.tools/v3/icmd/exitcode.go new file mode 100644 index 00000000..751254a0 --- /dev/null +++ b/vendor/gotest.tools/v3/icmd/exitcode.go @@ -0,0 +1,32 @@ +package icmd + +import ( + "syscall" + + "github.com/pkg/errors" + exec "golang.org/x/sys/execabs" +) + +// getExitCode returns the ExitStatus of a process from the error returned by +// exec.Run(). If the exit status could not be parsed an error is returned. +func getExitCode(err error) (int, error) { + if exiterr, ok := err.(*exec.ExitError); ok { + if procExit, ok := exiterr.Sys().(syscall.WaitStatus); ok { + return procExit.ExitStatus(), nil + } + } + return 0, errors.Wrap(err, "failed to get exit code") +} + +func processExitCode(err error) (exitCode int) { + if err == nil { + return 0 + } + exitCode, exiterr := getExitCode(err) + if exiterr != nil { + // TODO: Fix this so we check the error's text. + // we've failed to retrieve exit code, so we set it to 127 + return 127 + } + return exitCode +} diff --git a/vendor/gotest.tools/v3/icmd/ops.go b/vendor/gotest.tools/v3/icmd/ops.go new file mode 100644 index 00000000..35c3958d --- /dev/null +++ b/vendor/gotest.tools/v3/icmd/ops.go @@ -0,0 +1,46 @@ +package icmd + +import ( + "io" + "os" + "time" +) + +// CmdOp is an operation which modified a Cmd structure used to execute commands +type CmdOp func(*Cmd) + +// WithTimeout sets the timeout duration of the command +func WithTimeout(timeout time.Duration) CmdOp { + return func(c *Cmd) { + c.Timeout = timeout + } +} + +// WithEnv sets the environment variable of the command. +// Each arguments are in the form of KEY=VALUE +func WithEnv(env ...string) CmdOp { + return func(c *Cmd) { + c.Env = env + } +} + +// Dir sets the working directory of the command +func Dir(path string) CmdOp { + return func(c *Cmd) { + c.Dir = path + } +} + +// WithStdin sets the standard input of the command to the specified reader +func WithStdin(r io.Reader) CmdOp { + return func(c *Cmd) { + c.Stdin = r + } +} + +// WithExtraFile adds a file descriptor to the command +func WithExtraFile(f *os.File) CmdOp { + return func(c *Cmd) { + c.ExtraFiles = append(c.ExtraFiles, f) + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 1048af16..20613cce 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -587,6 +587,7 @@ gopkg.in/yaml.v3 gotest.tools/v3/assert gotest.tools/v3/assert/cmp gotest.tools/v3/golden +gotest.tools/v3/icmd gotest.tools/v3/internal/assert gotest.tools/v3/internal/difflib gotest.tools/v3/internal/format