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