1/*
2Copyright 2017 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package testingexec
18
19import (
20	"context"
21	"fmt"
22	"io"
23
24	"k8s.io/utils/exec"
25)
26
27// FakeExec is a simple scripted Interface type.
28type FakeExec struct {
29	CommandScript []FakeCommandAction
30	CommandCalls  int
31	LookPathFunc  func(string) (string, error)
32	// ExactOrder enforces that commands are called in the order they are scripted,
33	// and with the exact same arguments
34	ExactOrder bool
35	// DisableScripts removes the requirement that a slice of FakeCommandAction be
36	// populated before calling Command(). This makes the fakeexec (and subsequent
37	// calls to Run() or CombinedOutput() always return success and there is no
38	// ability to set their output.
39	DisableScripts bool
40}
41
42var _ exec.Interface = &FakeExec{}
43
44// FakeCommandAction is the function to be executed
45type FakeCommandAction func(cmd string, args ...string) exec.Cmd
46
47// Command is to track the commands that are executed
48func (fake *FakeExec) Command(cmd string, args ...string) exec.Cmd {
49	if fake.DisableScripts {
50		fakeCmd := &FakeCmd{DisableScripts: true}
51		return InitFakeCmd(fakeCmd, cmd, args...)
52	}
53	if fake.CommandCalls > len(fake.CommandScript)-1 {
54		panic(fmt.Sprintf("ran out of Command() actions. Could not handle command [%d]: %s args: %v", fake.CommandCalls, cmd, args))
55	}
56	i := fake.CommandCalls
57	fake.CommandCalls++
58	fakeCmd := fake.CommandScript[i](cmd, args...)
59	if fake.ExactOrder {
60		argv := append([]string{cmd}, args...)
61		fc := fakeCmd.(*FakeCmd)
62		if cmd != fc.Argv[0] {
63			panic(fmt.Sprintf("received command: %s, expected: %s", cmd, fc.Argv[0]))
64		}
65		if len(argv) != len(fc.Argv) {
66			panic(fmt.Sprintf("command (%s) received with extra/missing arguments. Expected %v, Received %v", cmd, fc.Argv, argv))
67		}
68		for i, a := range argv[1:] {
69			if a != fc.Argv[i+1] {
70				panic(fmt.Sprintf("command (%s) called with unexpected argument. Expected %s, Received %s", cmd, fc.Argv[i+1], a))
71			}
72		}
73	}
74	return fakeCmd
75}
76
77// CommandContext wraps arguments into exec.Cmd
78func (fake *FakeExec) CommandContext(ctx context.Context, cmd string, args ...string) exec.Cmd {
79	return fake.Command(cmd, args...)
80}
81
82// LookPath is for finding the path of a file
83func (fake *FakeExec) LookPath(file string) (string, error) {
84	return fake.LookPathFunc(file)
85}
86
87// FakeCmd is a simple scripted Cmd type.
88type FakeCmd struct {
89	Argv                 []string
90	CombinedOutputScript []FakeAction
91	CombinedOutputCalls  int
92	CombinedOutputLog    [][]string
93	OutputScript         []FakeAction
94	OutputCalls          int
95	OutputLog            [][]string
96	RunScript            []FakeAction
97	RunCalls             int
98	RunLog               [][]string
99	Dirs                 []string
100	Stdin                io.Reader
101	Stdout               io.Writer
102	Stderr               io.Writer
103	Env                  []string
104	StdoutPipeResponse   FakeStdIOPipeResponse
105	StderrPipeResponse   FakeStdIOPipeResponse
106	WaitResponse         error
107	StartResponse        error
108	DisableScripts       bool
109}
110
111var _ exec.Cmd = &FakeCmd{}
112
113// InitFakeCmd is for creating a fake exec.Cmd
114func InitFakeCmd(fake *FakeCmd, cmd string, args ...string) exec.Cmd {
115	fake.Argv = append([]string{cmd}, args...)
116	return fake
117}
118
119// FakeStdIOPipeResponse holds responses to use as fakes for the StdoutPipe and
120// StderrPipe method calls
121type FakeStdIOPipeResponse struct {
122	ReadCloser io.ReadCloser
123	Error      error
124}
125
126// FakeAction is a function type
127type FakeAction func() ([]byte, []byte, error)
128
129// SetDir sets the directory
130func (fake *FakeCmd) SetDir(dir string) {
131	fake.Dirs = append(fake.Dirs, dir)
132}
133
134// SetStdin sets the stdin
135func (fake *FakeCmd) SetStdin(in io.Reader) {
136	fake.Stdin = in
137}
138
139// SetStdout sets the stdout
140func (fake *FakeCmd) SetStdout(out io.Writer) {
141	fake.Stdout = out
142}
143
144// SetStderr sets the stderr
145func (fake *FakeCmd) SetStderr(out io.Writer) {
146	fake.Stderr = out
147}
148
149// SetEnv sets the environment variables
150func (fake *FakeCmd) SetEnv(env []string) {
151	fake.Env = env
152}
153
154// StdoutPipe returns an injected ReadCloser & error (via StdoutPipeResponse)
155// to be able to inject an output stream on Stdout
156func (fake *FakeCmd) StdoutPipe() (io.ReadCloser, error) {
157	return fake.StdoutPipeResponse.ReadCloser, fake.StdoutPipeResponse.Error
158}
159
160// StderrPipe returns an injected ReadCloser & error (via StderrPipeResponse)
161// to be able to inject an output stream on Stderr
162func (fake *FakeCmd) StderrPipe() (io.ReadCloser, error) {
163	return fake.StderrPipeResponse.ReadCloser, fake.StderrPipeResponse.Error
164}
165
166// Start mimicks starting the process (in the background) and returns the
167// injected StartResponse
168func (fake *FakeCmd) Start() error {
169	return fake.StartResponse
170}
171
172// Wait mimicks waiting for the process to exit returns the
173// injected WaitResponse
174func (fake *FakeCmd) Wait() error {
175	return fake.WaitResponse
176}
177
178// Run runs the command
179func (fake *FakeCmd) Run() error {
180	if fake.DisableScripts {
181		return nil
182	}
183	if fake.RunCalls > len(fake.RunScript)-1 {
184		panic("ran out of Run() actions")
185	}
186	if fake.RunLog == nil {
187		fake.RunLog = [][]string{}
188	}
189	i := fake.RunCalls
190	fake.RunLog = append(fake.RunLog, append([]string{}, fake.Argv...))
191	fake.RunCalls++
192	stdout, stderr, err := fake.RunScript[i]()
193	if stdout != nil {
194		fake.Stdout.Write(stdout)
195	}
196	if stderr != nil {
197		fake.Stderr.Write(stderr)
198	}
199	return err
200}
201
202// CombinedOutput returns the output from the command
203func (fake *FakeCmd) CombinedOutput() ([]byte, error) {
204	if fake.DisableScripts {
205		return []byte{}, nil
206	}
207	if fake.CombinedOutputCalls > len(fake.CombinedOutputScript)-1 {
208		panic("ran out of CombinedOutput() actions")
209	}
210	if fake.CombinedOutputLog == nil {
211		fake.CombinedOutputLog = [][]string{}
212	}
213	i := fake.CombinedOutputCalls
214	fake.CombinedOutputLog = append(fake.CombinedOutputLog, append([]string{}, fake.Argv...))
215	fake.CombinedOutputCalls++
216	stdout, _, err := fake.CombinedOutputScript[i]()
217	return stdout, err
218}
219
220// Output is the response from the command
221func (fake *FakeCmd) Output() ([]byte, error) {
222	if fake.DisableScripts {
223		return []byte{}, nil
224	}
225	if fake.OutputCalls > len(fake.OutputScript)-1 {
226		panic("ran out of Output() actions")
227	}
228	if fake.OutputLog == nil {
229		fake.OutputLog = [][]string{}
230	}
231	i := fake.OutputCalls
232	fake.OutputLog = append(fake.OutputLog, append([]string{}, fake.Argv...))
233	fake.OutputCalls++
234	stdout, _, err := fake.OutputScript[i]()
235	return stdout, err
236}
237
238// Stop is to stop the process
239func (fake *FakeCmd) Stop() {
240	// no-op
241}
242
243// FakeExitError is a simple fake ExitError type.
244type FakeExitError struct {
245	Status int
246}
247
248var _ exec.ExitError = FakeExitError{}
249
250func (fake FakeExitError) String() string {
251	return fmt.Sprintf("exit %d", fake.Status)
252}
253
254func (fake FakeExitError) Error() string {
255	return fake.String()
256}
257
258// Exited always returns true
259func (fake FakeExitError) Exited() bool {
260	return true
261}
262
263// ExitStatus returns the fake status
264func (fake FakeExitError) ExitStatus() int {
265	return fake.Status
266}
267