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  }