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