github.com/inspektor-gadget/inspektor-gadget@v0.28.1/pkg/testing/command/command.go (about) 1 // Copyright 2019-2024 The Inspektor Gadget authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package command provides a generic way for running testing commands. 16 package command 17 18 import ( 19 "bytes" 20 "errors" 21 "os/exec" 22 "syscall" 23 "testing" 24 25 "github.com/stretchr/testify/require" 26 ) 27 28 type Command struct { 29 // Name of the command to be run, used to give information. 30 Name string 31 32 // ValidateOutput is a function used to verify the output. It must make the test fail in 33 // case of error. 34 ValidateOutput func(t *testing.T, output string) 35 36 // StartAndStop indicates this command should first be started then stopped. 37 // It corresponds to gadget like execsnoop which wait user to type Ctrl^C. 38 StartAndStop bool 39 40 // started indicates this command was started. 41 // It is only used by command which have StartAndStop set. 42 started bool 43 44 // Cmd object is used when we want to start the command, then do 45 // other stuff and wait for its completion or just run the command. 46 Cmd *exec.Cmd 47 48 // stdout contains command standard output when started using Startcommand(). 49 stdout bytes.Buffer 50 51 // stderr contains command standard output when started using Startcommand(). 52 stderr bytes.Buffer 53 } 54 55 func (c *Command) IsStartAndStop() bool { 56 return c.StartAndStop 57 } 58 59 func (c *Command) Running() bool { 60 return c.started 61 } 62 63 // initExecCmd configures c.Cmd to store the stdout and stderr in c.stdout and c.stderr so that we 64 // can use them on c.verifyOutput(). 65 func (c *Command) initExecCmd() { 66 c.Cmd.Stdout = &c.stdout 67 c.Cmd.Stderr = &c.stderr 68 69 // To be able to kill the process of /bin/sh and its child (the process of 70 // c.Cmd), we need to send the termination signal to their process group ID 71 // (PGID). However, child processes get the same PGID as their parents by 72 // default, so in order to avoid killing also the integration tests process, 73 // we set the fields Setpgid and Pgid of syscall.SysProcAttr before 74 // executing /bin/sh. Doing so, the PGID of /bin/sh (and its children) 75 // will be set to its process ID, see: 76 // https://cs.opensource.google/go/go/+/refs/tags/go1.17.8:src/syscall/exec_linux.go;l=32-34. 77 c.Cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Pgid: 0} 78 } 79 80 // verifyOutput verifies the output of the command by using the 81 // ValidateOutput callback function provided by the user. 82 func (c *Command) verifyOutput(t *testing.T) { 83 if c.ValidateOutput != nil { 84 c.ValidateOutput(t, c.stdout.String()) 85 } 86 } 87 88 // Run runs the Command on the given as parameter test. 89 func (c *Command) Run(t *testing.T) { 90 c.initExecCmd() 91 92 t.Logf("Run command(%s):\n", c.Name) 93 err := c.Cmd.Run() 94 t.Logf("Command returned(%s):\n%s\n%s\n", 95 c.Name, c.stderr.String(), c.stdout.String()) 96 require.NoError(t, err, "failed to run command(%s)", c.Name) 97 98 c.verifyOutput(t) 99 } 100 101 // Start starts the Command on the given as parameter test, you need to 102 // wait it using Stop(). 103 func (c *Command) Start(t *testing.T) { 104 if c.started { 105 t.Logf("Warn(%s): trying to start command but it was already started\n", c.Name) 106 return 107 } 108 109 c.initExecCmd() 110 111 t.Logf("Start command(%s)", c.Name) 112 err := c.Cmd.Start() 113 require.NoError(t, err, "failed to start command(%s)", c.Name) 114 115 c.started = true 116 } 117 118 // Stop stops a Command previously started with Start(). 119 // To do so, it Kill() the process corresponding to this Cmd and then wait for 120 // its termination. 121 // Cmd output is then checked with regard to ExpectedString, ExpectedRegexp or ExpectedEntries. 122 func (c *Command) Stop(t *testing.T) { 123 if !c.started { 124 t.Logf("Warn(%s): trying to stop command but it was not started\n", c.Name) 125 return 126 } 127 128 t.Logf("Stop command(%s)\n", c.Name) 129 err := c.kill() 130 t.Logf("Command returned(%s):\n%s\n%s\n", 131 c.Name, c.stderr.String(), c.stdout.String()) 132 require.NoError(t, err, "failed to kill command(%s)", c.Name) 133 134 c.verifyOutput(t) 135 136 c.started = false 137 } 138 139 // kill kills a command by sending SIGKILL because we want to stop the process 140 // immediatly and avoid that the signal is trapped. 141 func (c *Command) kill() error { 142 const sig syscall.Signal = syscall.SIGKILL 143 144 // No need to kill, command has not been executed yet or it already exited 145 if c.Cmd == nil || (c.Cmd.ProcessState != nil && c.Cmd.ProcessState.Exited()) { 146 return nil 147 } 148 149 // Given that we set Setpgid, here we just need to send the PID of c.Cmd 150 // (which is the same PGID) as a negative number to syscall.Kill(). As a 151 // result, the signal will be received by all the processes with such PGID, 152 // in our case, the process of /bin/sh and c.Cmd. 153 err := syscall.Kill(-c.Cmd.Process.Pid, sig) 154 if err != nil { 155 return err 156 } 157 158 // In some cases, we do not have to wait here because the cmd was executed 159 // with run(), which already waits. On the contrary, in the case it was 160 // executed with start() thus ig.started is true, we need to wait indeed. 161 if c.started { 162 err = c.Cmd.Wait() 163 if err == nil { 164 return nil 165 } 166 167 // Verify if the error is about the signal we just sent. In that case, 168 // do not return error, it is what we were expecting. 169 var exiterr *exec.ExitError 170 if ok := errors.As(err, &exiterr); !ok { 171 return err 172 } 173 174 waitStatus, ok := exiterr.Sys().(syscall.WaitStatus) 175 if !ok { 176 return err 177 } 178 179 if waitStatus.Signal() != sig { 180 return err 181 } 182 183 return nil 184 } 185 186 return err 187 }