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 `