github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/drivers/shared/executor/executor_test.go (about)

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