github.com/containers/podman/v2@v2.2.2-0.20210501105131-c1e07d070c4c/pkg/hooks/exec/exec_test.go (about)

     1  package exec
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"os"
     8  	"runtime"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  
    13  	rspec "github.com/opencontainers/runtime-spec/specs-go"
    14  	"github.com/stretchr/testify/assert"
    15  )
    16  
    17  // path is the path to an example hook executable.
    18  var path string
    19  
    20  // unavoidableEnvironmentKeys may be injected even if the hook
    21  // executable is executed with a requested empty environment.
    22  var unavoidableEnvironmentKeys []string
    23  
    24  func TestRun(t *testing.T) {
    25  	ctx := context.Background()
    26  	hook := &rspec.Hook{
    27  		Path: path,
    28  		Args: []string{"sh", "-c", "cat"},
    29  	}
    30  	var stderr, stdout bytes.Buffer
    31  	hookErr, err := Run(ctx, hook, []byte("{}"), &stdout, &stderr, DefaultPostKillTimeout)
    32  	if err != nil {
    33  		t.Fatal(err)
    34  	}
    35  	if hookErr != nil {
    36  		t.Fatal(hookErr)
    37  	}
    38  	assert.Equal(t, "{}", stdout.String())
    39  	assert.Equal(t, "", stderr.String())
    40  }
    41  
    42  func TestRunIgnoreOutput(t *testing.T) {
    43  	ctx := context.Background()
    44  	hook := &rspec.Hook{
    45  		Path: path,
    46  		Args: []string{"sh", "-c", "cat"},
    47  	}
    48  	hookErr, err := Run(ctx, hook, []byte("{}"), nil, nil, DefaultPostKillTimeout)
    49  	if err != nil {
    50  		t.Fatal(err)
    51  	}
    52  	if hookErr != nil {
    53  		t.Fatal(hookErr)
    54  	}
    55  }
    56  
    57  func TestRunFailedStart(t *testing.T) {
    58  	ctx := context.Background()
    59  	hook := &rspec.Hook{
    60  		Path: "/does/not/exist",
    61  	}
    62  	hookErr, err := Run(ctx, hook, []byte("{}"), nil, nil, DefaultPostKillTimeout)
    63  	if err == nil {
    64  		t.Fatal("unexpected success")
    65  	}
    66  	if !os.IsNotExist(err) {
    67  		t.Fatal(err)
    68  	}
    69  	assert.Equal(t, err, hookErr)
    70  }
    71  
    72  func parseEnvironment(input string) (env map[string]string, err error) {
    73  	env = map[string]string{}
    74  	lines := strings.Split(input, "\n")
    75  	for i, line := range lines {
    76  		if line == "" && i == len(lines)-1 {
    77  			continue // no content after the terminal newline
    78  		}
    79  		keyValue := strings.SplitN(line, "=", 2)
    80  		if len(keyValue) < 2 {
    81  			return env, fmt.Errorf("no = in environment line: %q", line)
    82  		}
    83  		env[keyValue[0]] = keyValue[1]
    84  	}
    85  	for _, key := range unavoidableEnvironmentKeys {
    86  		delete(env, key)
    87  	}
    88  	return env, nil
    89  }
    90  
    91  func TestRunEnvironment(t *testing.T) {
    92  	ctx := context.Background()
    93  	hook := &rspec.Hook{
    94  		Path: path,
    95  		Args: []string{"sh", "-c", "env"},
    96  	}
    97  	for _, tt := range []struct {
    98  		name     string
    99  		env      []string
   100  		expected map[string]string
   101  	}{
   102  		{
   103  			name:     "unset",
   104  			expected: map[string]string{},
   105  		},
   106  		{
   107  			name:     "set empty",
   108  			env:      []string{},
   109  			expected: map[string]string{},
   110  		},
   111  		{
   112  			name: "set",
   113  			env: []string{
   114  				"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
   115  				"TERM=xterm",
   116  			},
   117  			expected: map[string]string{
   118  				"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
   119  				"TERM": "xterm",
   120  			},
   121  		},
   122  	} {
   123  		test := tt
   124  		t.Run(test.name, func(t *testing.T) {
   125  			var stderr, stdout bytes.Buffer
   126  			hook.Env = test.env
   127  			hookErr, err := Run(ctx, hook, []byte("{}"), &stdout, &stderr, DefaultPostKillTimeout)
   128  			if err != nil {
   129  				t.Fatal(err)
   130  			}
   131  			if hookErr != nil {
   132  				t.Fatal(hookErr)
   133  			}
   134  			assert.Equal(t, "", stderr.String())
   135  
   136  			env, err := parseEnvironment(stdout.String())
   137  			if err != nil {
   138  				t.Fatal(err)
   139  			}
   140  			assert.Equal(t, test.expected, env)
   141  		})
   142  	}
   143  }
   144  
   145  func TestRunCancel(t *testing.T) {
   146  	hook := &rspec.Hook{
   147  		Path: path,
   148  		Args: []string{"sh", "-c", "echo waiting; sleep 2; echo done"},
   149  	}
   150  	one := 1
   151  	for _, tt := range []struct {
   152  		name              string
   153  		contextTimeout    time.Duration
   154  		hookTimeout       *int
   155  		expectedHookError string
   156  		expectedRunError  error
   157  		expectedStdout    string
   158  	}{
   159  		{
   160  			name:           "no timeouts",
   161  			expectedStdout: "waiting\ndone\n",
   162  		},
   163  		{
   164  			name:              "context timeout",
   165  			contextTimeout:    time.Duration(1) * time.Second,
   166  			expectedStdout:    "waiting\n",
   167  			expectedHookError: "^executing \\[sh -c echo waiting; sleep 2; echo done]: signal: killed$",
   168  			expectedRunError:  context.DeadlineExceeded,
   169  		},
   170  		{
   171  			name:              "hook timeout",
   172  			hookTimeout:       &one,
   173  			expectedStdout:    "waiting\n",
   174  			expectedHookError: "^executing \\[sh -c echo waiting; sleep 2; echo done]: signal: killed$",
   175  			expectedRunError:  context.DeadlineExceeded,
   176  		},
   177  	} {
   178  		test := tt
   179  		t.Run(test.name, func(t *testing.T) {
   180  			ctx := context.Background()
   181  			var stderr, stdout bytes.Buffer
   182  			if test.contextTimeout > 0 {
   183  				var cancel context.CancelFunc
   184  				ctx, cancel = context.WithTimeout(ctx, test.contextTimeout)
   185  				defer cancel()
   186  			}
   187  			hook.Timeout = test.hookTimeout
   188  			hookErr, err := Run(ctx, hook, []byte("{}"), &stdout, &stderr, DefaultPostKillTimeout)
   189  			assert.Equal(t, test.expectedRunError, err)
   190  			if test.expectedHookError == "" {
   191  				if hookErr != nil {
   192  					t.Fatal(hookErr)
   193  				}
   194  			} else {
   195  				assert.Regexp(t, test.expectedHookError, hookErr.Error())
   196  			}
   197  			assert.Equal(t, "", stderr.String())
   198  			assert.Equal(t, test.expectedStdout, stdout.String())
   199  		})
   200  	}
   201  }
   202  
   203  func TestRunKillTimeout(t *testing.T) {
   204  	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(500)*time.Millisecond)
   205  	defer cancel()
   206  	hook := &rspec.Hook{
   207  		Path: path,
   208  		Args: []string{"sh", "-c", "sleep 1"},
   209  	}
   210  	hookErr, err := Run(ctx, hook, []byte("{}"), nil, nil, time.Duration(0))
   211  	assert.Equal(t, context.DeadlineExceeded, err)
   212  	assert.Regexp(t, "^(failed to reap process within 0s of the kill signal|executing \\[sh -c sleep 1]: signal: killed)$", hookErr)
   213  }
   214  
   215  func init() {
   216  	if runtime.GOOS != "windows" {
   217  		path = "/bin/sh"
   218  		unavoidableEnvironmentKeys = []string{"PWD", "SHLVL", "_"}
   219  	} else {
   220  		panic("we need a reliable executable path on Windows")
   221  	}
   222  }