github.com/hernad/nomad@v1.6.112/drivers/shared/executor/executor_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package executor
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"path/filepath"
    13  	"runtime"
    14  	"strings"
    15  	"sync"
    16  	"syscall"
    17  	"testing"
    18  	"time"
    19  
    20  	"github.com/hashicorp/go-hclog"
    21  	"github.com/hernad/nomad/ci"
    22  	"github.com/hernad/nomad/client/allocdir"
    23  	"github.com/hernad/nomad/client/lib/cgutil"
    24  	"github.com/hernad/nomad/client/taskenv"
    25  	"github.com/hernad/nomad/client/testutil"
    26  	"github.com/hernad/nomad/helper/testlog"
    27  	"github.com/hernad/nomad/nomad/mock"
    28  	"github.com/hernad/nomad/nomad/structs"
    29  	"github.com/hernad/nomad/plugins/drivers"
    30  	tu "github.com/hernad/nomad/testutil"
    31  	ps "github.com/mitchellh/go-ps"
    32  	"github.com/stretchr/testify/assert"
    33  	"github.com/stretchr/testify/require"
    34  )
    35  
    36  var executorFactories = map[string]executorFactory{}
    37  
    38  type executorFactory struct {
    39  	new              func(hclog.Logger, uint64) Executor
    40  	configureExecCmd func(*testing.T, *ExecCommand)
    41  }
    42  
    43  var universalFactory = executorFactory{
    44  	new:              NewExecutor,
    45  	configureExecCmd: func(*testing.T, *ExecCommand) {},
    46  }
    47  
    48  func init() {
    49  	executorFactories["UniversalExecutor"] = universalFactory
    50  }
    51  
    52  type testExecCmd struct {
    53  	command  *ExecCommand
    54  	allocDir *allocdir.AllocDir
    55  
    56  	stdout         *bytes.Buffer
    57  	stderr         *bytes.Buffer
    58  	outputCopyDone *sync.WaitGroup
    59  }
    60  
    61  // testExecutorContext returns an ExecutorContext and AllocDir.
    62  //
    63  // The caller is responsible for calling AllocDir.Destroy() to cleanup.
    64  func testExecutorCommand(t *testing.T) *testExecCmd {
    65  	alloc := mock.Alloc()
    66  	task := alloc.Job.TaskGroups[0].Tasks[0]
    67  	taskEnv := taskenv.NewBuilder(mock.Node(), alloc, task, "global").Build()
    68  
    69  	allocDir := allocdir.NewAllocDir(testlog.HCLogger(t), t.TempDir(), alloc.ID)
    70  	if err := allocDir.Build(); err != nil {
    71  		t.Fatalf("AllocDir.Build() failed: %v", err)
    72  	}
    73  	if err := allocDir.NewTaskDir(task.Name).Build(false, nil); err != nil {
    74  		allocDir.Destroy()
    75  		t.Fatalf("allocDir.NewTaskDir(%q) failed: %v", task.Name, err)
    76  	}
    77  	td := allocDir.TaskDirs[task.Name]
    78  	cmd := &ExecCommand{
    79  		Env:     taskEnv.List(),
    80  		TaskDir: td.Dir,
    81  		Resources: &drivers.Resources{
    82  			NomadResources: &structs.AllocatedTaskResources{
    83  				Cpu: structs.AllocatedCpuResources{
    84  					CpuShares: 500,
    85  				},
    86  				Memory: structs.AllocatedMemoryResources{
    87  					MemoryMB: 256,
    88  				},
    89  			},
    90  			LinuxResources: &drivers.LinuxResources{
    91  				CPUShares:        500,
    92  				MemoryLimitBytes: 256 * 1024 * 1024,
    93  			},
    94  		},
    95  	}
    96  
    97  	if cgutil.UseV2 {
    98  		cmd.Resources.LinuxResources.CpusetCgroupPath = filepath.Join(cgutil.CgroupRoot, "testing.scope", cgutil.CgroupScope(alloc.ID, task.Name))
    99  	}
   100  
   101  	testCmd := &testExecCmd{
   102  		command:  cmd,
   103  		allocDir: allocDir,
   104  	}
   105  	configureTLogging(t, testCmd)
   106  	return testCmd
   107  }
   108  
   109  // configureTLogging configures a test command executor with buffer as Std{out|err}
   110  // but using os.Pipe so it mimics non-test case where cmd is set with files as Std{out|err}
   111  // the buffers can be used to read command output
   112  func configureTLogging(t *testing.T, testcmd *testExecCmd) {
   113  	var stdout, stderr bytes.Buffer
   114  	var copyDone sync.WaitGroup
   115  
   116  	stdoutPr, stdoutPw, err := os.Pipe()
   117  	require.NoError(t, err)
   118  
   119  	stderrPr, stderrPw, err := os.Pipe()
   120  	require.NoError(t, err)
   121  
   122  	copyDone.Add(2)
   123  	go func() {
   124  		defer copyDone.Done()
   125  		io.Copy(&stdout, stdoutPr)
   126  	}()
   127  	go func() {
   128  		defer copyDone.Done()
   129  		io.Copy(&stderr, stderrPr)
   130  	}()
   131  
   132  	testcmd.stdout = &stdout
   133  	testcmd.stderr = &stderr
   134  	testcmd.outputCopyDone = &copyDone
   135  
   136  	testcmd.command.stdout = stdoutPw
   137  	testcmd.command.stderr = stderrPw
   138  	return
   139  }
   140  
   141  func TestExecutor_Start_Invalid(t *testing.T) {
   142  	ci.Parallel(t)
   143  	invalid := "/bin/foobar"
   144  	for name, factory := range executorFactories {
   145  		t.Run(name, func(t *testing.T) {
   146  			require := require.New(t)
   147  			testExecCmd := testExecutorCommand(t)
   148  			execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   149  			execCmd.Cmd = invalid
   150  			execCmd.Args = []string{"1"}
   151  			factory.configureExecCmd(t, execCmd)
   152  			defer allocDir.Destroy()
   153  			executor := factory.new(testlog.HCLogger(t), 0)
   154  			defer executor.Shutdown("", 0)
   155  
   156  			_, err := executor.Launch(execCmd)
   157  			require.Error(err)
   158  		})
   159  	}
   160  }
   161  
   162  func TestExecutor_Start_Wait_Failure_Code(t *testing.T) {
   163  	ci.Parallel(t)
   164  	for name, factory := range executorFactories {
   165  		t.Run(name, func(t *testing.T) {
   166  			require := require.New(t)
   167  			testExecCmd := testExecutorCommand(t)
   168  			execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   169  			execCmd.Cmd = "/bin/sh"
   170  			execCmd.Args = []string{"-c", "sleep 1; /bin/date fail"}
   171  			factory.configureExecCmd(t, execCmd)
   172  			defer allocDir.Destroy()
   173  			executor := factory.new(testlog.HCLogger(t), 0)
   174  			defer executor.Shutdown("", 0)
   175  
   176  			ps, err := executor.Launch(execCmd)
   177  			require.NoError(err)
   178  			require.NotZero(ps.Pid)
   179  			ps, _ = executor.Wait(context.Background())
   180  			require.NotZero(ps.ExitCode, "expected exit code to be non zero")
   181  			require.NoError(executor.Shutdown("SIGINT", 100*time.Millisecond))
   182  		})
   183  	}
   184  }
   185  
   186  func TestExecutor_Start_Wait(t *testing.T) {
   187  	ci.Parallel(t)
   188  	for name, factory := range executorFactories {
   189  		t.Run(name, func(t *testing.T) {
   190  			require := require.New(t)
   191  			testExecCmd := testExecutorCommand(t)
   192  			execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   193  			execCmd.Cmd = "/bin/echo"
   194  			execCmd.Args = []string{"hello world"}
   195  			factory.configureExecCmd(t, execCmd)
   196  
   197  			defer allocDir.Destroy()
   198  			executor := factory.new(testlog.HCLogger(t), 0)
   199  			defer executor.Shutdown("", 0)
   200  
   201  			ps, err := executor.Launch(execCmd)
   202  			require.NoError(err)
   203  			require.NotZero(ps.Pid)
   204  
   205  			ps, err = executor.Wait(context.Background())
   206  			require.NoError(err)
   207  			require.NoError(executor.Shutdown("SIGINT", 100*time.Millisecond))
   208  
   209  			expected := "hello world"
   210  			tu.WaitForResult(func() (bool, error) {
   211  				act := strings.TrimSpace(string(testExecCmd.stdout.String()))
   212  				if expected != act {
   213  					return false, fmt.Errorf("expected: '%s' actual: '%s'", expected, act)
   214  				}
   215  				return true, nil
   216  			}, func(err error) {
   217  				require.NoError(err)
   218  			})
   219  		})
   220  	}
   221  }
   222  
   223  func TestExecutor_Start_Wait_Children(t *testing.T) {
   224  	ci.Parallel(t)
   225  	for name, factory := range executorFactories {
   226  		t.Run(name, func(t *testing.T) {
   227  			require := require.New(t)
   228  			testExecCmd := testExecutorCommand(t)
   229  			execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   230  			execCmd.Cmd = "/bin/sh"
   231  			execCmd.Args = []string{"-c", "(sleep 30 > /dev/null & ) ; exec sleep 1"}
   232  			factory.configureExecCmd(t, execCmd)
   233  
   234  			defer allocDir.Destroy()
   235  			executor := factory.new(testlog.HCLogger(t), 0)
   236  			defer executor.Shutdown("SIGKILL", 0)
   237  
   238  			ps, err := executor.Launch(execCmd)
   239  			require.NoError(err)
   240  			require.NotZero(ps.Pid)
   241  
   242  			ch := make(chan error)
   243  
   244  			go func() {
   245  				ps, err = executor.Wait(context.Background())
   246  				t.Logf("Processe completed with %#v error: %#v", ps, err)
   247  				ch <- err
   248  			}()
   249  
   250  			timeout := 7 * time.Second
   251  			select {
   252  			case <-ch:
   253  				require.NoError(err)
   254  				//good
   255  			case <-time.After(timeout):
   256  				require.Fail(fmt.Sprintf("process is running after timeout: %v", timeout))
   257  			}
   258  		})
   259  	}
   260  }
   261  
   262  func TestExecutor_WaitExitSignal(t *testing.T) {
   263  	ci.Parallel(t)
   264  	testutil.CgroupsCompatibleV1(t) // todo(shoenig) #12351
   265  
   266  	for name, factory := range executorFactories {
   267  		t.Run(name, func(t *testing.T) {
   268  			testExecCmd := testExecutorCommand(t)
   269  			execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   270  			execCmd.Cmd = "/bin/sleep"
   271  			execCmd.Args = []string{"10000"}
   272  			execCmd.ResourceLimits = true
   273  			factory.configureExecCmd(t, execCmd)
   274  
   275  			defer allocDir.Destroy()
   276  			executor := factory.new(testlog.HCLogger(t), 0)
   277  			defer executor.Shutdown("", 0)
   278  
   279  			pState, err := executor.Launch(execCmd)
   280  			require.NoError(t, err)
   281  
   282  			go func() {
   283  				tu.WaitForResult(func() (bool, error) {
   284  					ch, err := executor.Stats(context.Background(), time.Second)
   285  					if err != nil {
   286  						return false, err
   287  					}
   288  					select {
   289  					case <-time.After(time.Second):
   290  						return false, fmt.Errorf("stats failed to send on interval")
   291  					case ru := <-ch:
   292  						assert.NotEmpty(t, ru.Pids, "no pids recorded in stats")
   293  
   294  						// just checking we measured something; each executor type has its own abilities,
   295  						// and e.g. cgroup v2 provides different information than cgroup v1
   296  						assert.NotEmpty(t, ru.ResourceUsage.MemoryStats.Measured)
   297  
   298  						assert.WithinDuration(t, time.Now(), time.Unix(0, ru.Timestamp), time.Second)
   299  					}
   300  					proc, err := os.FindProcess(pState.Pid)
   301  					if err != nil {
   302  						return false, err
   303  					}
   304  					err = proc.Signal(syscall.SIGKILL)
   305  					if err != nil {
   306  						return false, err
   307  					}
   308  					return true, nil
   309  				}, func(err error) {
   310  					assert.NoError(t, executor.Signal(os.Kill))
   311  					assert.NoError(t, err)
   312  				})
   313  			}()
   314  
   315  			pState, err = executor.Wait(context.Background())
   316  			require.NoError(t, err)
   317  			require.Equal(t, pState.Signal, int(syscall.SIGKILL))
   318  		})
   319  	}
   320  }
   321  
   322  func TestExecutor_Start_Kill(t *testing.T) {
   323  	ci.Parallel(t)
   324  	for name, factory := range executorFactories {
   325  		t.Run(name, func(t *testing.T) {
   326  			require := require.New(t)
   327  			testExecCmd := testExecutorCommand(t)
   328  			execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   329  			execCmd.Cmd = "/bin/sleep"
   330  			execCmd.Args = []string{"10"}
   331  			factory.configureExecCmd(t, execCmd)
   332  
   333  			defer allocDir.Destroy()
   334  			executor := factory.new(testlog.HCLogger(t), 0)
   335  			defer executor.Shutdown("", 0)
   336  
   337  			ps, err := executor.Launch(execCmd)
   338  			require.NoError(err)
   339  			require.NotZero(ps.Pid)
   340  
   341  			require.NoError(executor.Shutdown("SIGINT", 100*time.Millisecond))
   342  
   343  			time.Sleep(time.Duration(tu.TestMultiplier()*2) * time.Second)
   344  			output := testExecCmd.stdout.String()
   345  			expected := ""
   346  			act := strings.TrimSpace(string(output))
   347  			if act != expected {
   348  				t.Fatalf("Command output incorrectly: want %v; got %v", expected, act)
   349  			}
   350  		})
   351  	}
   352  }
   353  
   354  func TestExecutor_Shutdown_Exit(t *testing.T) {
   355  	ci.Parallel(t)
   356  	require := require.New(t)
   357  	testExecCmd := testExecutorCommand(t)
   358  	execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   359  	execCmd.Cmd = "/bin/sleep"
   360  	execCmd.Args = []string{"100"}
   361  	cfg := &ExecutorConfig{
   362  		LogFile: "/dev/null",
   363  	}
   364  	executor, pluginClient, err := CreateExecutor(testlog.HCLogger(t), nil, cfg)
   365  	require.NoError(err)
   366  
   367  	proc, err := executor.Launch(execCmd)
   368  	require.NoError(err)
   369  	require.NotZero(proc.Pid)
   370  
   371  	executor.Shutdown("", 0)
   372  	pluginClient.Kill()
   373  	tu.WaitForResult(func() (bool, error) {
   374  		p, err := ps.FindProcess(proc.Pid)
   375  		if err != nil {
   376  			return false, err
   377  		}
   378  		return p == nil, fmt.Errorf("process found: %d", proc.Pid)
   379  	}, func(err error) {
   380  		require.NoError(err)
   381  	})
   382  	require.NoError(allocDir.Destroy())
   383  }
   384  
   385  func TestUniversalExecutor_MakeExecutable(t *testing.T) {
   386  	ci.Parallel(t)
   387  	// Create a temp file
   388  	f, err := os.CreateTemp("", "")
   389  	if err != nil {
   390  		t.Fatal(err)
   391  	}
   392  	defer f.Close()
   393  	defer os.Remove(f.Name())
   394  
   395  	// Set its permissions to be non-executable
   396  	f.Chmod(os.FileMode(0610))
   397  
   398  	err = makeExecutable(f.Name())
   399  	if err != nil {
   400  		t.Fatalf("makeExecutable() failed: %v", err)
   401  	}
   402  
   403  	// Check the permissions
   404  	stat, err := f.Stat()
   405  	if err != nil {
   406  		t.Fatalf("Stat() failed: %v", err)
   407  	}
   408  
   409  	act := stat.Mode().Perm()
   410  	exp := os.FileMode(0755)
   411  	if act != exp {
   412  		t.Fatalf("expected permissions %v; got %v", exp, act)
   413  	}
   414  }
   415  
   416  func TestUniversalExecutor_LookupPath(t *testing.T) {
   417  	ci.Parallel(t)
   418  	require := require.New(t)
   419  	// Create a temp dir
   420  	tmpDir := t.TempDir()
   421  
   422  	// Make a foo subdir
   423  	os.MkdirAll(filepath.Join(tmpDir, "foo"), 0700)
   424  
   425  	// Write a file under foo
   426  	filePath := filepath.Join(tmpDir, "foo", "tmp.txt")
   427  	err := os.WriteFile(filePath, []byte{1, 2}, os.ModeAppend)
   428  	require.Nil(err)
   429  
   430  	// Lookup with full path on host to binary
   431  	path, err := lookupBin("not_tmpDir", filePath)
   432  	require.Nil(err)
   433  	require.Equal(filePath, path)
   434  
   435  	// Lookout with an absolute path to the binary
   436  	_, err = lookupBin(tmpDir, "/foo/tmp.txt")
   437  	require.Nil(err)
   438  
   439  	// Write a file under task dir
   440  	filePath3 := filepath.Join(tmpDir, "tmp.txt")
   441  	os.WriteFile(filePath3, []byte{1, 2}, os.ModeAppend)
   442  
   443  	// Lookup with file name, should find the one we wrote above
   444  	path, err = lookupBin(tmpDir, "tmp.txt")
   445  	require.Nil(err)
   446  	require.Equal(filepath.Join(tmpDir, "tmp.txt"), path)
   447  
   448  	// Write a file under local subdir
   449  	os.MkdirAll(filepath.Join(tmpDir, "local"), 0700)
   450  	filePath2 := filepath.Join(tmpDir, "local", "tmp.txt")
   451  	os.WriteFile(filePath2, []byte{1, 2}, os.ModeAppend)
   452  
   453  	// Lookup with file name, should find the one we wrote above
   454  	path, err = lookupBin(tmpDir, "tmp.txt")
   455  	require.Nil(err)
   456  	require.Equal(filepath.Join(tmpDir, "local", "tmp.txt"), path)
   457  
   458  	// Lookup a host path
   459  	_, err = lookupBin(tmpDir, "/bin/sh")
   460  	require.NoError(err)
   461  
   462  	// Lookup a host path via $PATH
   463  	_, err = lookupBin(tmpDir, "sh")
   464  	require.NoError(err)
   465  }
   466  
   467  // setupRoootfs setups the rootfs for libcontainer executor
   468  // It uses busybox to make some binaries available - somewhat cheaper
   469  // than mounting the underlying host filesystem
   470  func setupRootfs(t *testing.T, rootfs string) {
   471  	paths := []string{
   472  		"/bin/sh",
   473  		"/bin/sleep",
   474  		"/bin/echo",
   475  		"/bin/date",
   476  	}
   477  
   478  	for _, p := range paths {
   479  		setupRootfsBinary(t, rootfs, p)
   480  	}
   481  }
   482  
   483  // setupRootfsBinary installs a busybox link in the desired path
   484  func setupRootfsBinary(t *testing.T, rootfs, path string) {
   485  	t.Helper()
   486  
   487  	dst := filepath.Join(rootfs, path)
   488  	err := os.MkdirAll(filepath.Dir(dst), 0755)
   489  	require.NoError(t, err)
   490  
   491  	src := filepath.Join(
   492  		"test-resources", "busybox",
   493  		fmt.Sprintf("busybox-%s", runtime.GOARCH),
   494  	)
   495  
   496  	err = os.Link(src, dst)
   497  	if err != nil {
   498  		// On failure, fallback to copying the file directly.
   499  		// Linking may fail if the test source code lives on a separate
   500  		// volume/partition from the temp dir used for testing
   501  		copyFile(t, src, dst)
   502  	}
   503  }
   504  
   505  func copyFile(t *testing.T, src, dst string) {
   506  	in, err := os.Open(src)
   507  	require.NoErrorf(t, err, "copying %v -> %v", src, dst)
   508  	defer in.Close()
   509  
   510  	ins, err := in.Stat()
   511  	require.NoErrorf(t, err, "copying %v -> %v", src, dst)
   512  
   513  	out, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE, ins.Mode())
   514  	require.NoErrorf(t, err, "copying %v -> %v", src, dst)
   515  	defer func() {
   516  		if err := out.Close(); err != nil {
   517  			t.Fatalf("copying %v -> %v failed: %v", src, dst, err)
   518  		}
   519  	}()
   520  
   521  	_, err = io.Copy(out, in)
   522  	require.NoErrorf(t, err, "copying %v -> %v", src, dst)
   523  }
   524  
   525  // TestExecutor_Start_Kill_Immediately_NoGrace asserts that executors shutdown
   526  // immediately when sent a kill signal with no grace period.
   527  func TestExecutor_Start_Kill_Immediately_NoGrace(t *testing.T) {
   528  	ci.Parallel(t)
   529  	for name, factory := range executorFactories {
   530  
   531  		t.Run(name, func(t *testing.T) {
   532  			require := require.New(t)
   533  			testExecCmd := testExecutorCommand(t)
   534  			execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   535  			execCmd.Cmd = "/bin/sleep"
   536  			execCmd.Args = []string{"100"}
   537  			factory.configureExecCmd(t, execCmd)
   538  			defer allocDir.Destroy()
   539  			executor := factory.new(testlog.HCLogger(t), 0)
   540  			defer executor.Shutdown("", 0)
   541  
   542  			ps, err := executor.Launch(execCmd)
   543  			require.NoError(err)
   544  			require.NotZero(ps.Pid)
   545  
   546  			waitCh := make(chan interface{})
   547  			go func() {
   548  				defer close(waitCh)
   549  				executor.Wait(context.Background())
   550  			}()
   551  
   552  			require.NoError(executor.Shutdown("SIGKILL", 0))
   553  
   554  			select {
   555  			case <-waitCh:
   556  				// all good!
   557  			case <-time.After(4 * time.Second * time.Duration(tu.TestMultiplier())):
   558  				require.Fail("process did not terminate despite SIGKILL")
   559  			}
   560  		})
   561  	}
   562  }
   563  
   564  func TestExecutor_Start_Kill_Immediately_WithGrace(t *testing.T) {
   565  	ci.Parallel(t)
   566  	for name, factory := range executorFactories {
   567  		t.Run(name, func(t *testing.T) {
   568  			require := require.New(t)
   569  			testExecCmd := testExecutorCommand(t)
   570  			execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   571  			execCmd.Cmd = "/bin/sleep"
   572  			execCmd.Args = []string{"100"}
   573  			factory.configureExecCmd(t, execCmd)
   574  			defer allocDir.Destroy()
   575  			executor := factory.new(testlog.HCLogger(t), 0)
   576  			defer executor.Shutdown("", 0)
   577  
   578  			ps, err := executor.Launch(execCmd)
   579  			require.NoError(err)
   580  			require.NotZero(ps.Pid)
   581  
   582  			waitCh := make(chan interface{})
   583  			go func() {
   584  				defer close(waitCh)
   585  				executor.Wait(context.Background())
   586  			}()
   587  
   588  			require.NoError(executor.Shutdown("SIGKILL", 100*time.Millisecond))
   589  
   590  			select {
   591  			case <-waitCh:
   592  				// all good!
   593  			case <-time.After(4 * time.Second * time.Duration(tu.TestMultiplier())):
   594  				require.Fail("process did not terminate despite SIGKILL")
   595  			}
   596  		})
   597  	}
   598  }
   599  
   600  // TestExecutor_Start_NonExecutableBinaries asserts that executor marks binary as executable
   601  // before starting
   602  func TestExecutor_Start_NonExecutableBinaries(t *testing.T) {
   603  	ci.Parallel(t)
   604  
   605  	for name, factory := range executorFactories {
   606  		t.Run(name, func(t *testing.T) {
   607  			require := require.New(t)
   608  
   609  			tmpDir := t.TempDir()
   610  
   611  			nonExecutablePath := filepath.Join(tmpDir, "nonexecutablefile")
   612  			os.WriteFile(nonExecutablePath,
   613  				[]byte("#!/bin/sh\necho hello world"),
   614  				0600)
   615  
   616  			testExecCmd := testExecutorCommand(t)
   617  			execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   618  			execCmd.Cmd = nonExecutablePath
   619  			factory.configureExecCmd(t, execCmd)
   620  
   621  			executor := factory.new(testlog.HCLogger(t), 0)
   622  			defer executor.Shutdown("", 0)
   623  
   624  			// need to configure path in chroot with that file if using isolation executor
   625  			if _, ok := executor.(*UniversalExecutor); !ok {
   626  				taskName := filepath.Base(testExecCmd.command.TaskDir)
   627  				err := allocDir.NewTaskDir(taskName).Build(true, map[string]string{
   628  					tmpDir: tmpDir,
   629  				})
   630  				require.NoError(err)
   631  			}
   632  
   633  			defer allocDir.Destroy()
   634  			ps, err := executor.Launch(execCmd)
   635  			require.NoError(err)
   636  			require.NotZero(ps.Pid)
   637  
   638  			ps, err = executor.Wait(context.Background())
   639  			require.NoError(err)
   640  			require.NoError(executor.Shutdown("SIGINT", 100*time.Millisecond))
   641  
   642  			expected := "hello world"
   643  			tu.WaitForResult(func() (bool, error) {
   644  				act := strings.TrimSpace(string(testExecCmd.stdout.String()))
   645  				if expected != act {
   646  					return false, fmt.Errorf("expected: '%s' actual: '%s'", expected, act)
   647  				}
   648  				return true, nil
   649  			}, func(err error) {
   650  				stderr := strings.TrimSpace(string(testExecCmd.stderr.String()))
   651  				t.Logf("stderr: %v", stderr)
   652  				require.NoError(err)
   653  			})
   654  		})
   655  	}
   656  }