github.com/dorkamotorka/go/src@v0.0.0-20230614113921-187095f0e316/os/exec/lp_windows_test.go (about)

     1  // Copyright 2013 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Use an external test to avoid os/exec -> internal/testenv -> os/exec
     6  // circular dependency.
     7  
     8  package exec_test
     9  
    10  import (
    11  	"errors"
    12  	"fmt"
    13  	"internal/testenv"
    14  	"io"
    15  	"os"
    16  	"os/exec"
    17  	"path/filepath"
    18  	"strconv"
    19  	"strings"
    20  	"testing"
    21  )
    22  
    23  func init() {
    24  	registerHelperCommand("exec", cmdExec)
    25  	registerHelperCommand("lookpath", cmdLookPath)
    26  }
    27  
    28  func cmdLookPath(args ...string) {
    29  	p, err := exec.LookPath(args[0])
    30  	if err != nil {
    31  		fmt.Fprintf(os.Stderr, "LookPath failed: %v\n", err)
    32  		os.Exit(1)
    33  	}
    34  	fmt.Print(p)
    35  }
    36  
    37  func cmdExec(args ...string) {
    38  	cmd := exec.Command(args[1])
    39  	cmd.Dir = args[0]
    40  	if errors.Is(cmd.Err, exec.ErrDot) {
    41  		cmd.Err = nil
    42  	}
    43  	output, err := cmd.CombinedOutput()
    44  	if err != nil {
    45  		fmt.Fprintf(os.Stderr, "Child: %s %s", err, string(output))
    46  		os.Exit(1)
    47  	}
    48  	fmt.Printf("%s", string(output))
    49  }
    50  
    51  func installExe(t *testing.T, dest, src string) {
    52  	fsrc, err := os.Open(src)
    53  	if err != nil {
    54  		t.Fatal("os.Open failed: ", err)
    55  	}
    56  	defer fsrc.Close()
    57  	fdest, err := os.Create(dest)
    58  	if err != nil {
    59  		t.Fatal("os.Create failed: ", err)
    60  	}
    61  	defer fdest.Close()
    62  	_, err = io.Copy(fdest, fsrc)
    63  	if err != nil {
    64  		t.Fatal("io.Copy failed: ", err)
    65  	}
    66  }
    67  
    68  func installBat(t *testing.T, dest string) {
    69  	f, err := os.Create(dest)
    70  	if err != nil {
    71  		t.Fatalf("failed to create batch file: %v", err)
    72  	}
    73  	defer f.Close()
    74  	fmt.Fprintf(f, "@echo %s\n", dest)
    75  }
    76  
    77  func installProg(t *testing.T, dest, srcExe string) {
    78  	err := os.MkdirAll(filepath.Dir(dest), 0700)
    79  	if err != nil {
    80  		t.Fatal("os.MkdirAll failed: ", err)
    81  	}
    82  	if strings.ToLower(filepath.Ext(dest)) == ".bat" {
    83  		installBat(t, dest)
    84  		return
    85  	}
    86  	installExe(t, dest, srcExe)
    87  }
    88  
    89  type lookPathTest struct {
    90  	rootDir   string
    91  	PATH      string
    92  	PATHEXT   string
    93  	files     []string
    94  	searchFor string
    95  	fails     bool // test is expected to fail
    96  }
    97  
    98  func (test lookPathTest) runProg(t *testing.T, env []string, cmd *exec.Cmd) (string, error) {
    99  	cmd.Env = env
   100  	cmd.Dir = test.rootDir
   101  	args := append([]string(nil), cmd.Args...)
   102  	args[0] = filepath.Base(args[0])
   103  	cmdText := fmt.Sprintf("%q command", strings.Join(args, " "))
   104  	out, err := cmd.CombinedOutput()
   105  	if (err != nil) != test.fails {
   106  		if test.fails {
   107  			t.Fatalf("test=%+v: %s succeeded, but expected to fail", test, cmdText)
   108  		}
   109  		t.Fatalf("test=%+v: %s failed, but expected to succeed: %v - %v", test, cmdText, err, string(out))
   110  	}
   111  	if err != nil {
   112  		return "", fmt.Errorf("test=%+v: %s failed: %v - %v", test, cmdText, err, string(out))
   113  	}
   114  	// normalise program output
   115  	p := string(out)
   116  	// trim terminating \r and \n that batch file outputs
   117  	for len(p) > 0 && (p[len(p)-1] == '\n' || p[len(p)-1] == '\r') {
   118  		p = p[:len(p)-1]
   119  	}
   120  	if !filepath.IsAbs(p) {
   121  		return p, nil
   122  	}
   123  	if p[:len(test.rootDir)] != test.rootDir {
   124  		t.Fatalf("test=%+v: %s output is wrong: %q must have %q prefix", test, cmdText, p, test.rootDir)
   125  	}
   126  	return p[len(test.rootDir)+1:], nil
   127  }
   128  
   129  func updateEnv(env []string, name, value string) []string {
   130  	for i, e := range env {
   131  		if strings.HasPrefix(strings.ToUpper(e), name+"=") {
   132  			env[i] = name + "=" + value
   133  			return env
   134  		}
   135  	}
   136  	return append(env, name+"="+value)
   137  }
   138  
   139  func createEnv(dir, PATH, PATHEXT string) []string {
   140  	env := os.Environ()
   141  	env = updateEnv(env, "PATHEXT", PATHEXT)
   142  	// Add dir in front of every directory in the PATH.
   143  	dirs := filepath.SplitList(PATH)
   144  	for i := range dirs {
   145  		dirs[i] = filepath.Join(dir, dirs[i])
   146  	}
   147  	path := strings.Join(dirs, ";")
   148  	env = updateEnv(env, "PATH", os.Getenv("SystemRoot")+"/System32;"+path)
   149  	return env
   150  }
   151  
   152  // createFiles copies srcPath file into multiply files.
   153  // It uses dir as prefix for all destination files.
   154  func createFiles(t *testing.T, dir string, files []string, srcPath string) {
   155  	for _, f := range files {
   156  		installProg(t, filepath.Join(dir, f), srcPath)
   157  	}
   158  }
   159  
   160  func (test lookPathTest) run(t *testing.T, tmpdir, printpathExe string) {
   161  	test.rootDir = tmpdir
   162  	createFiles(t, test.rootDir, test.files, printpathExe)
   163  	env := createEnv(test.rootDir, test.PATH, test.PATHEXT)
   164  	// Run "cmd.exe /c test.searchFor" with new environment and
   165  	// work directory set. All candidates are copies of printpath.exe.
   166  	// These will output their program paths when run.
   167  	should, errCmd := test.runProg(t, env, testenv.Command(t, "cmd", "/c", test.searchFor))
   168  	// Run the lookpath program with new environment and work directory set.
   169  	have, errLP := test.runProg(t, env, helperCommand(t, "lookpath", test.searchFor))
   170  	// Compare results.
   171  	if errCmd == nil && errLP == nil {
   172  		// both succeeded
   173  		if should != have {
   174  			t.Fatalf("test=%+v:\ncmd /c ran: %s\nlookpath found: %s", test, should, have)
   175  		}
   176  		return
   177  	}
   178  	if errCmd != nil && errLP != nil {
   179  		// both failed -> continue
   180  		return
   181  	}
   182  	if errCmd != nil {
   183  		t.Fatal(errCmd)
   184  	}
   185  	if errLP != nil {
   186  		t.Fatal(errLP)
   187  	}
   188  }
   189  
   190  var lookPathTests = []lookPathTest{
   191  	{
   192  		PATHEXT:   `.COM;.EXE;.BAT`,
   193  		PATH:      `p1;p2`,
   194  		files:     []string{`p1\a.exe`, `p2\a.exe`, `p2\a`},
   195  		searchFor: `a`,
   196  	},
   197  	{
   198  		PATHEXT:   `.COM;.EXE;.BAT`,
   199  		PATH:      `p1.dir;p2.dir`,
   200  		files:     []string{`p1.dir\a`, `p2.dir\a.exe`},
   201  		searchFor: `a`,
   202  	},
   203  	{
   204  		PATHEXT:   `.COM;.EXE;.BAT`,
   205  		PATH:      `p1;p2`,
   206  		files:     []string{`p1\a.exe`, `p2\a.exe`},
   207  		searchFor: `a.exe`,
   208  	},
   209  	{
   210  		PATHEXT:   `.COM;.EXE;.BAT`,
   211  		PATH:      `p1;p2`,
   212  		files:     []string{`p1\a.exe`, `p2\b.exe`},
   213  		searchFor: `b`,
   214  	},
   215  	{
   216  		PATHEXT:   `.COM;.EXE;.BAT`,
   217  		PATH:      `p1;p2`,
   218  		files:     []string{`p1\b`, `p2\a`},
   219  		searchFor: `a`,
   220  		fails:     true, // TODO(brainman): do not know why this fails
   221  	},
   222  	// If the command name specifies a path, the shell searches
   223  	// the specified path for an executable file matching
   224  	// the command name. If a match is found, the external
   225  	// command (the executable file) executes.
   226  	{
   227  		PATHEXT:   `.COM;.EXE;.BAT`,
   228  		PATH:      `p1;p2`,
   229  		files:     []string{`p1\a.exe`, `p2\a.exe`},
   230  		searchFor: `p2\a`,
   231  	},
   232  	// If the command name specifies a path, the shell searches
   233  	// the specified path for an executable file matching the command
   234  	// name. ... If no match is found, the shell reports an error
   235  	// and command processing completes.
   236  	{
   237  		PATHEXT:   `.COM;.EXE;.BAT`,
   238  		PATH:      `p1;p2`,
   239  		files:     []string{`p1\b.exe`, `p2\a.exe`},
   240  		searchFor: `p2\b`,
   241  		fails:     true,
   242  	},
   243  	// If the command name does not specify a path, the shell
   244  	// searches the current directory for an executable file
   245  	// matching the command name. If a match is found, the external
   246  	// command (the executable file) executes.
   247  	{
   248  		PATHEXT:   `.COM;.EXE;.BAT`,
   249  		PATH:      `p1;p2`,
   250  		files:     []string{`a`, `p1\a.exe`, `p2\a.exe`},
   251  		searchFor: `a`,
   252  	},
   253  	// The shell now searches each directory specified by the
   254  	// PATH environment variable, in the order listed, for an
   255  	// executable file matching the command name. If a match
   256  	// is found, the external command (the executable file) executes.
   257  	{
   258  		PATHEXT:   `.COM;.EXE;.BAT`,
   259  		PATH:      `p1;p2`,
   260  		files:     []string{`p1\a.exe`, `p2\a.exe`},
   261  		searchFor: `a`,
   262  	},
   263  	// The shell now searches each directory specified by the
   264  	// PATH environment variable, in the order listed, for an
   265  	// executable file matching the command name. If no match
   266  	// is found, the shell reports an error and command processing
   267  	// completes.
   268  	{
   269  		PATHEXT:   `.COM;.EXE;.BAT`,
   270  		PATH:      `p1;p2`,
   271  		files:     []string{`p1\a.exe`, `p2\a.exe`},
   272  		searchFor: `b`,
   273  		fails:     true,
   274  	},
   275  	// If the command name includes a file extension, the shell
   276  	// searches each directory for the exact file name specified
   277  	// by the command name.
   278  	{
   279  		PATHEXT:   `.COM;.EXE;.BAT`,
   280  		PATH:      `p1;p2`,
   281  		files:     []string{`p1\a.exe`, `p2\a.exe`},
   282  		searchFor: `a.exe`,
   283  	},
   284  	{
   285  		PATHEXT:   `.COM;.EXE;.BAT`,
   286  		PATH:      `p1;p2`,
   287  		files:     []string{`p1\a.exe`, `p2\a.exe`},
   288  		searchFor: `a.com`,
   289  		fails:     true, // includes extension and not exact file name match
   290  	},
   291  	{
   292  		PATHEXT:   `.COM;.EXE;.BAT`,
   293  		PATH:      `p1`,
   294  		files:     []string{`p1\a.exe.exe`},
   295  		searchFor: `a.exe`,
   296  	},
   297  	{
   298  		PATHEXT:   `.COM;.BAT`,
   299  		PATH:      `p1;p2`,
   300  		files:     []string{`p1\a.exe`, `p2\a.exe`},
   301  		searchFor: `a.exe`,
   302  	},
   303  	// If the command name does not include a file extension, the shell
   304  	// adds the extensions listed in the PATHEXT environment variable,
   305  	// one by one, and searches the directory for that file name. Note
   306  	// that the shell tries all possible file extensions in a specific
   307  	// directory before moving on to search the next directory
   308  	// (if there is one).
   309  	{
   310  		PATHEXT:   `.COM;.EXE`,
   311  		PATH:      `p1;p2`,
   312  		files:     []string{`p1\a.bat`, `p2\a.exe`},
   313  		searchFor: `a`,
   314  	},
   315  	{
   316  		PATHEXT:   `.COM;.EXE;.BAT`,
   317  		PATH:      `p1;p2`,
   318  		files:     []string{`p1\a.bat`, `p2\a.exe`},
   319  		searchFor: `a`,
   320  	},
   321  	{
   322  		PATHEXT:   `.COM;.EXE;.BAT`,
   323  		PATH:      `p1;p2`,
   324  		files:     []string{`p1\a.bat`, `p1\a.exe`, `p2\a.bat`, `p2\a.exe`},
   325  		searchFor: `a`,
   326  	},
   327  	{
   328  		PATHEXT:   `.COM`,
   329  		PATH:      `p1;p2`,
   330  		files:     []string{`p1\a.bat`, `p2\a.exe`},
   331  		searchFor: `a`,
   332  		fails:     true, // tried all extensions in PATHEXT, but none matches
   333  	},
   334  }
   335  
   336  func TestLookPathWindows(t *testing.T) {
   337  	if testing.Short() {
   338  		maySkipHelperCommand("lookpath")
   339  		t.Skipf("skipping test in short mode that would build a helper binary")
   340  	}
   341  	t.Parallel()
   342  
   343  	tmp := t.TempDir()
   344  	printpathExe := buildPrintPathExe(t, tmp)
   345  
   346  	// Run all tests.
   347  	for i, test := range lookPathTests {
   348  		i, test := i, test
   349  		t.Run(fmt.Sprint(i), func(t *testing.T) {
   350  			t.Parallel()
   351  
   352  			dir := filepath.Join(tmp, "d"+strconv.Itoa(i))
   353  			err := os.Mkdir(dir, 0700)
   354  			if err != nil {
   355  				t.Fatal("Mkdir failed: ", err)
   356  			}
   357  			test.run(t, dir, printpathExe)
   358  		})
   359  	}
   360  }
   361  
   362  type commandTest struct {
   363  	PATH  string
   364  	files []string
   365  	dir   string
   366  	arg0  string
   367  	want  string
   368  	fails bool // test is expected to fail
   369  }
   370  
   371  func (test commandTest) isSuccess(rootDir, output string, err error) error {
   372  	if err != nil {
   373  		return fmt.Errorf("test=%+v: exec: %v %v", test, err, output)
   374  	}
   375  	path := output
   376  	if path[:len(rootDir)] != rootDir {
   377  		return fmt.Errorf("test=%+v: %q must have %q prefix", test, path, rootDir)
   378  	}
   379  	path = path[len(rootDir)+1:]
   380  	if path != test.want {
   381  		return fmt.Errorf("test=%+v: want %q, got %q", test, test.want, path)
   382  	}
   383  	return nil
   384  }
   385  
   386  func (test commandTest) runOne(t *testing.T, rootDir string, env []string, dir, arg0 string) {
   387  	cmd := helperCommand(t, "exec", dir, arg0)
   388  	cmd.Dir = rootDir
   389  	cmd.Env = env
   390  	output, err := cmd.CombinedOutput()
   391  	err = test.isSuccess(rootDir, string(output), err)
   392  	if (err != nil) != test.fails {
   393  		if test.fails {
   394  			t.Errorf("test=%+v: succeeded, but expected to fail", test)
   395  		} else {
   396  			t.Error(err)
   397  		}
   398  	}
   399  }
   400  
   401  func (test commandTest) run(t *testing.T, rootDir, printpathExe string) {
   402  	createFiles(t, rootDir, test.files, printpathExe)
   403  	PATHEXT := `.COM;.EXE;.BAT`
   404  	env := createEnv(rootDir, test.PATH, PATHEXT)
   405  	test.runOne(t, rootDir, env, test.dir, test.arg0)
   406  }
   407  
   408  var commandTests = []commandTest{
   409  	// testing commands with no slash, like `a.exe`
   410  	{
   411  		// should find a.exe in current directory
   412  		files: []string{`a.exe`},
   413  		arg0:  `a.exe`,
   414  		want:  `a.exe`,
   415  	},
   416  	{
   417  		// like above, but add PATH in attempt to break the test
   418  		PATH:  `p2;p`,
   419  		files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`},
   420  		arg0:  `a.exe`,
   421  		want:  `a.exe`,
   422  	},
   423  	{
   424  		// like above, but use "a" instead of "a.exe" for command
   425  		PATH:  `p2;p`,
   426  		files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`},
   427  		arg0:  `a`,
   428  		want:  `a.exe`,
   429  	},
   430  	// testing commands with slash, like `.\a.exe`
   431  	{
   432  		// should find p\a.exe
   433  		files: []string{`p\a.exe`},
   434  		arg0:  `p\a.exe`,
   435  		want:  `p\a.exe`,
   436  	},
   437  	{
   438  		// like above, but adding `.` in front of executable should still be OK
   439  		files: []string{`p\a.exe`},
   440  		arg0:  `.\p\a.exe`,
   441  		want:  `p\a.exe`,
   442  	},
   443  	{
   444  		// like above, but with PATH added in attempt to break it
   445  		PATH:  `p2`,
   446  		files: []string{`p\a.exe`, `p2\a.exe`},
   447  		arg0:  `p\a.exe`,
   448  		want:  `p\a.exe`,
   449  	},
   450  	{
   451  		// like above, but make sure .exe is tried even for commands with slash
   452  		PATH:  `p2`,
   453  		files: []string{`p\a.exe`, `p2\a.exe`},
   454  		arg0:  `p\a`,
   455  		want:  `p\a.exe`,
   456  	},
   457  	// tests commands, like `a.exe`, with c.Dir set
   458  	{
   459  		// should not find a.exe in p, because LookPath(`a.exe`) will fail
   460  		files: []string{`p\a.exe`},
   461  		dir:   `p`,
   462  		arg0:  `a.exe`,
   463  		want:  `p\a.exe`,
   464  		fails: true,
   465  	},
   466  	{
   467  		// LookPath(`a.exe`) will find `.\a.exe`, but prefixing that with
   468  		// dir `p\a.exe` will refer to a non-existent file
   469  		files: []string{`a.exe`, `p\not_important_file`},
   470  		dir:   `p`,
   471  		arg0:  `a.exe`,
   472  		want:  `a.exe`,
   473  		fails: true,
   474  	},
   475  	{
   476  		// like above, but making test succeed by installing file
   477  		// in referred destination (so LookPath(`a.exe`) will still
   478  		// find `.\a.exe`, but we successfully execute `p\a.exe`)
   479  		files: []string{`a.exe`, `p\a.exe`},
   480  		dir:   `p`,
   481  		arg0:  `a.exe`,
   482  		want:  `p\a.exe`,
   483  	},
   484  	{
   485  		// like above, but add PATH in attempt to break the test
   486  		PATH:  `p2;p`,
   487  		files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`},
   488  		dir:   `p`,
   489  		arg0:  `a.exe`,
   490  		want:  `p\a.exe`,
   491  	},
   492  	{
   493  		// like above, but use "a" instead of "a.exe" for command
   494  		PATH:  `p2;p`,
   495  		files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`},
   496  		dir:   `p`,
   497  		arg0:  `a`,
   498  		want:  `p\a.exe`,
   499  	},
   500  	{
   501  		// finds `a.exe` in the PATH regardless of dir set
   502  		// because LookPath returns full path in that case
   503  		PATH:  `p2;p`,
   504  		files: []string{`p\a.exe`, `p2\a.exe`},
   505  		dir:   `p`,
   506  		arg0:  `a.exe`,
   507  		want:  `p2\a.exe`,
   508  	},
   509  	// tests commands, like `.\a.exe`, with c.Dir set
   510  	{
   511  		// should use dir when command is path, like ".\a.exe"
   512  		files: []string{`p\a.exe`},
   513  		dir:   `p`,
   514  		arg0:  `.\a.exe`,
   515  		want:  `p\a.exe`,
   516  	},
   517  	{
   518  		// like above, but with PATH added in attempt to break it
   519  		PATH:  `p2`,
   520  		files: []string{`p\a.exe`, `p2\a.exe`},
   521  		dir:   `p`,
   522  		arg0:  `.\a.exe`,
   523  		want:  `p\a.exe`,
   524  	},
   525  	{
   526  		// like above, but make sure .exe is tried even for commands with slash
   527  		PATH:  `p2`,
   528  		files: []string{`p\a.exe`, `p2\a.exe`},
   529  		dir:   `p`,
   530  		arg0:  `.\a`,
   531  		want:  `p\a.exe`,
   532  	},
   533  }
   534  
   535  func TestCommand(t *testing.T) {
   536  	if testing.Short() {
   537  		maySkipHelperCommand("exec")
   538  		t.Skipf("skipping test in short mode that would build a helper binary")
   539  	}
   540  	t.Parallel()
   541  
   542  	tmp := t.TempDir()
   543  	printpathExe := buildPrintPathExe(t, tmp)
   544  
   545  	// Run all tests.
   546  	for i, test := range commandTests {
   547  		i, test := i, test
   548  		t.Run(fmt.Sprint(i), func(t *testing.T) {
   549  			t.Parallel()
   550  
   551  			dir := filepath.Join(tmp, "d"+strconv.Itoa(i))
   552  			err := os.Mkdir(dir, 0700)
   553  			if err != nil {
   554  				t.Fatal("Mkdir failed: ", err)
   555  			}
   556  			test.run(t, dir, printpathExe)
   557  		})
   558  	}
   559  }
   560  
   561  // buildPrintPathExe creates a Go program that prints its own path.
   562  // dir is a temp directory where executable will be created.
   563  // The function returns full path to the created program.
   564  func buildPrintPathExe(t *testing.T, dir string) string {
   565  	const name = "printpath"
   566  	srcname := name + ".go"
   567  	err := os.WriteFile(filepath.Join(dir, srcname), []byte(printpathSrc), 0644)
   568  	if err != nil {
   569  		t.Fatalf("failed to create source: %v", err)
   570  	}
   571  	if err != nil {
   572  		t.Fatalf("failed to execute template: %v", err)
   573  	}
   574  	outname := name + ".exe"
   575  	cmd := testenv.Command(t, testenv.GoToolPath(t), "build", "-o", outname, srcname)
   576  	cmd.Dir = dir
   577  	out, err := cmd.CombinedOutput()
   578  	if err != nil {
   579  		t.Fatalf("failed to build executable: %v - %v", err, string(out))
   580  	}
   581  	return filepath.Join(dir, outname)
   582  }
   583  
   584  const printpathSrc = `
   585  package main
   586  
   587  import (
   588  	"os"
   589  	"syscall"
   590  	"unsafe"
   591  )
   592  
   593  func getMyName() (string, error) {
   594  	var sysproc = syscall.MustLoadDLL("kernel32.dll").MustFindProc("GetModuleFileNameW")
   595  	b := make([]uint16, syscall.MAX_PATH)
   596  	r, _, err := sysproc.Call(0, uintptr(unsafe.Pointer(&b[0])), uintptr(len(b)))
   597  	n := uint32(r)
   598  	if n == 0 {
   599  		return "", err
   600  	}
   601  	return syscall.UTF16ToString(b[0:n]), nil
   602  }
   603  
   604  func main() {
   605  	path, err := getMyName()
   606  	if err != nil {
   607  		os.Stderr.Write([]byte("getMyName failed: " + err.Error() + "\n"))
   608  		os.Exit(1)
   609  	}
   610  	os.Stdout.Write([]byte(path))
   611  }
   612  `