github.com/ice-blockchain/go/src@v0.0.0-20240403114104-1564d284e521/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 "io/fs" 16 "os" 17 "os/exec" 18 "path/filepath" 19 "slices" 20 "strings" 21 "testing" 22 ) 23 24 func init() { 25 registerHelperCommand("printpath", cmdPrintPath) 26 } 27 28 func cmdPrintPath(args ...string) { 29 exe, err := os.Executable() 30 if err != nil { 31 fmt.Fprintf(os.Stderr, "Executable: %v\n", err) 32 os.Exit(1) 33 } 34 fmt.Println(exe) 35 } 36 37 // makePATH returns a PATH variable referring to the 38 // given directories relative to a root directory. 39 // 40 // The empty string results in an empty entry. 41 // Paths beginning with . are kept as relative entries. 42 func makePATH(root string, dirs []string) string { 43 paths := make([]string, 0, len(dirs)) 44 for _, d := range dirs { 45 switch { 46 case d == "": 47 paths = append(paths, "") 48 case d == "." || (len(d) >= 2 && d[0] == '.' && os.IsPathSeparator(d[1])): 49 paths = append(paths, filepath.Clean(d)) 50 default: 51 paths = append(paths, filepath.Join(root, d)) 52 } 53 } 54 return strings.Join(paths, string(os.PathListSeparator)) 55 } 56 57 // installProgs creates executable files (or symlinks to executable files) at 58 // multiple destination paths. It uses root as prefix for all destination files. 59 func installProgs(t *testing.T, root string, files []string) { 60 for _, f := range files { 61 dstPath := filepath.Join(root, f) 62 63 dir := filepath.Dir(dstPath) 64 if err := os.MkdirAll(dir, 0755); err != nil { 65 t.Fatal(err) 66 } 67 68 if os.IsPathSeparator(f[len(f)-1]) { 69 continue // directory and PATH entry only. 70 } 71 if strings.EqualFold(filepath.Ext(f), ".bat") { 72 installBat(t, dstPath) 73 } else { 74 installExe(t, dstPath) 75 } 76 } 77 } 78 79 // installExe installs a copy of the test executable 80 // at the given location, creating directories as needed. 81 // 82 // (We use a copy instead of just a symlink to ensure that os.Executable 83 // always reports an unambiguous path, regardless of how it is implemented.) 84 func installExe(t *testing.T, dstPath string) { 85 src, err := os.Open(exePath(t)) 86 if err != nil { 87 t.Fatal(err) 88 } 89 defer src.Close() 90 91 dst, err := os.OpenFile(dstPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o777) 92 if err != nil { 93 t.Fatal(err) 94 } 95 defer func() { 96 if err := dst.Close(); err != nil { 97 t.Fatal(err) 98 } 99 }() 100 101 _, err = io.Copy(dst, src) 102 if err != nil { 103 t.Fatal(err) 104 } 105 } 106 107 // installBat creates a batch file at dst that prints its own 108 // path when run. 109 func installBat(t *testing.T, dstPath string) { 110 dst, err := os.OpenFile(dstPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o777) 111 if err != nil { 112 t.Fatal(err) 113 } 114 defer func() { 115 if err := dst.Close(); err != nil { 116 t.Fatal(err) 117 } 118 }() 119 120 if _, err := fmt.Fprintf(dst, "@echo %s\r\n", dstPath); err != nil { 121 t.Fatal(err) 122 } 123 } 124 125 type lookPathTest struct { 126 name string 127 PATHEXT string // empty to use default 128 files []string 129 PATH []string // if nil, use all parent directories from files 130 searchFor string 131 want string 132 wantErr error 133 skipCmdExeCheck bool // if true, do not check want against the behavior of cmd.exe 134 } 135 136 var lookPathTests = []lookPathTest{ 137 { 138 name: "first match", 139 files: []string{`p1\a.exe`, `p2\a.exe`, `p2\a`}, 140 searchFor: `a`, 141 want: `p1\a.exe`, 142 }, 143 { 144 name: "dirs with extensions", 145 files: []string{`p1.dir\a`, `p2.dir\a.exe`}, 146 searchFor: `a`, 147 want: `p2.dir\a.exe`, 148 }, 149 { 150 name: "first with extension", 151 files: []string{`p1\a.exe`, `p2\a.exe`}, 152 searchFor: `a.exe`, 153 want: `p1\a.exe`, 154 }, 155 { 156 name: "specific name", 157 files: []string{`p1\a.exe`, `p2\b.exe`}, 158 searchFor: `b`, 159 want: `p2\b.exe`, 160 }, 161 { 162 name: "no extension", 163 files: []string{`p1\b`, `p2\a`}, 164 searchFor: `a`, 165 wantErr: exec.ErrNotFound, 166 }, 167 { 168 name: "directory, no extension", 169 files: []string{`p1\a.exe`, `p2\a.exe`}, 170 searchFor: `p2\a`, 171 want: `p2\a.exe`, 172 }, 173 { 174 name: "no match", 175 files: []string{`p1\a.exe`, `p2\a.exe`}, 176 searchFor: `b`, 177 wantErr: exec.ErrNotFound, 178 }, 179 { 180 name: "no match with dir", 181 files: []string{`p1\b.exe`, `p2\a.exe`}, 182 searchFor: `p2\b`, 183 wantErr: exec.ErrNotFound, 184 }, 185 { 186 name: "extensionless file in CWD ignored", 187 files: []string{`a`, `p1\a.exe`, `p2\a.exe`}, 188 searchFor: `a`, 189 want: `p1\a.exe`, 190 }, 191 { 192 name: "extensionless file in PATH ignored", 193 files: []string{`p1\a`, `p2\a.exe`}, 194 searchFor: `a`, 195 want: `p2\a.exe`, 196 }, 197 { 198 name: "specific extension", 199 files: []string{`p1\a.exe`, `p2\a.bat`}, 200 searchFor: `a.bat`, 201 want: `p2\a.bat`, 202 }, 203 { 204 name: "mismatched extension", 205 files: []string{`p1\a.exe`, `p2\a.exe`}, 206 searchFor: `a.com`, 207 wantErr: exec.ErrNotFound, 208 }, 209 { 210 name: "doubled extension", 211 files: []string{`p1\a.exe.exe`}, 212 searchFor: `a.exe`, 213 want: `p1\a.exe.exe`, 214 }, 215 { 216 name: "extension not in PATHEXT", 217 PATHEXT: `.COM;.BAT`, 218 files: []string{`p1\a.exe`, `p2\a.exe`}, 219 searchFor: `a.exe`, 220 want: `p1\a.exe`, 221 }, 222 { 223 name: "first allowed by PATHEXT", 224 PATHEXT: `.COM;.EXE`, 225 files: []string{`p1\a.bat`, `p2\a.exe`}, 226 searchFor: `a`, 227 want: `p2\a.exe`, 228 }, 229 { 230 name: "first directory containing a PATHEXT match", 231 PATHEXT: `.COM;.EXE;.BAT`, 232 files: []string{`p1\a.bat`, `p2\a.exe`}, 233 searchFor: `a`, 234 want: `p1\a.bat`, 235 }, 236 { 237 name: "first PATHEXT entry", 238 PATHEXT: `.COM;.EXE;.BAT`, 239 files: []string{`p1\a.bat`, `p1\a.exe`, `p2\a.bat`, `p2\a.exe`}, 240 searchFor: `a`, 241 want: `p1\a.exe`, 242 }, 243 { 244 name: "ignore dir with PATHEXT extension", 245 files: []string{`a.exe\`}, 246 searchFor: `a`, 247 wantErr: exec.ErrNotFound, 248 }, 249 { 250 name: "ignore empty PATH entry", 251 files: []string{`a.bat`, `p\a.bat`}, 252 PATH: []string{`p`}, 253 searchFor: `a`, 254 want: `p\a.bat`, 255 // If cmd.exe is too old it might not respect NoDefaultCurrentDirectoryInExePath, 256 // so skip that check. 257 skipCmdExeCheck: true, 258 }, 259 { 260 name: "return ErrDot if found by a different absolute path", 261 files: []string{`p1\a.bat`, `p2\a.bat`}, 262 PATH: []string{`.\p1`, `p2`}, 263 searchFor: `a`, 264 want: `p1\a.bat`, 265 wantErr: exec.ErrDot, 266 }, 267 { 268 name: "suppress ErrDot if also found in absolute path", 269 files: []string{`p1\a.bat`, `p2\a.bat`}, 270 PATH: []string{`.\p1`, `p1`, `p2`}, 271 searchFor: `a`, 272 want: `p1\a.bat`, 273 }, 274 } 275 276 func TestLookPathWindows(t *testing.T) { 277 // Not parallel: uses Chdir and Setenv. 278 279 // We are using the "printpath" command mode to test exec.Command here, 280 // so we won't be calling helperCommand to resolve it. 281 // That may cause it to appear to be unused. 282 maySkipHelperCommand("printpath") 283 284 // Before we begin, find the absolute path to cmd.exe. 285 // In non-short mode, we will use it to check the ground truth 286 // of the test's "want" field. 287 cmdExe, err := exec.LookPath("cmd") 288 if err != nil { 289 t.Fatal(err) 290 } 291 292 for _, tt := range lookPathTests { 293 t.Run(tt.name, func(t *testing.T) { 294 if tt.want == "" && tt.wantErr == nil { 295 t.Fatalf("test must specify either want or wantErr") 296 } 297 298 root := t.TempDir() 299 installProgs(t, root, tt.files) 300 301 if tt.PATHEXT != "" { 302 t.Setenv("PATHEXT", tt.PATHEXT) 303 t.Logf("set PATHEXT=%s", tt.PATHEXT) 304 } 305 306 var pathVar string 307 if tt.PATH == nil { 308 paths := make([]string, 0, len(tt.files)) 309 for _, f := range tt.files { 310 dir := filepath.Join(root, filepath.Dir(f)) 311 if !slices.Contains(paths, dir) { 312 paths = append(paths, dir) 313 } 314 } 315 pathVar = strings.Join(paths, string(os.PathListSeparator)) 316 } else { 317 pathVar = makePATH(root, tt.PATH) 318 } 319 t.Setenv("PATH", pathVar) 320 t.Logf("set PATH=%s", pathVar) 321 322 chdir(t, root) 323 324 if !testing.Short() && !(tt.skipCmdExeCheck || errors.Is(tt.wantErr, exec.ErrDot)) { 325 // Check that cmd.exe, which is our source of ground truth, 326 // agrees that our test case is correct. 327 cmd := testenv.Command(t, cmdExe, "/c", tt.searchFor, "printpath") 328 out, err := cmd.Output() 329 if err == nil { 330 gotAbs := strings.TrimSpace(string(out)) 331 wantAbs := "" 332 if tt.want != "" { 333 wantAbs = filepath.Join(root, tt.want) 334 } 335 if gotAbs != wantAbs { 336 // cmd.exe disagrees. Probably the test case is wrong? 337 t.Fatalf("%v\n\tresolved to %s\n\twant %s", cmd, gotAbs, wantAbs) 338 } 339 } else if tt.wantErr == nil { 340 if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 { 341 t.Fatalf("%v: %v\n%s", cmd, err, ee.Stderr) 342 } 343 t.Fatalf("%v: %v", cmd, err) 344 } 345 } 346 347 got, err := exec.LookPath(tt.searchFor) 348 if filepath.IsAbs(got) { 349 got, err = filepath.Rel(root, got) 350 if err != nil { 351 t.Fatal(err) 352 } 353 } 354 if got != tt.want { 355 t.Errorf("LookPath(%#q) = %#q; want %#q", tt.searchFor, got, tt.want) 356 } 357 if !errors.Is(err, tt.wantErr) { 358 t.Errorf("LookPath(%#q): %v; want %v", tt.searchFor, err, tt.wantErr) 359 } 360 }) 361 } 362 } 363 364 type commandTest struct { 365 name string 366 PATH []string 367 files []string 368 dir string 369 arg0 string 370 want string 371 wantPath string // the resolved c.Path, if different from want 372 wantErrDot bool 373 wantRunErr error 374 } 375 376 var commandTests = []commandTest{ 377 // testing commands with no slash, like `a.exe` 378 { 379 name: "current directory", 380 files: []string{`a.exe`}, 381 PATH: []string{"."}, 382 arg0: `a.exe`, 383 want: `a.exe`, 384 wantErrDot: true, 385 }, 386 { 387 name: "with extra PATH", 388 files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`}, 389 PATH: []string{".", "p2", "p"}, 390 arg0: `a.exe`, 391 want: `a.exe`, 392 wantErrDot: true, 393 }, 394 { 395 name: "with extra PATH and no extension", 396 files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`}, 397 PATH: []string{".", "p2", "p"}, 398 arg0: `a`, 399 want: `a.exe`, 400 wantErrDot: true, 401 }, 402 // testing commands with slash, like `.\a.exe` 403 { 404 name: "with dir", 405 files: []string{`p\a.exe`}, 406 PATH: []string{"."}, 407 arg0: `p\a.exe`, 408 want: `p\a.exe`, 409 }, 410 { 411 name: "with explicit dot", 412 files: []string{`p\a.exe`}, 413 PATH: []string{"."}, 414 arg0: `.\p\a.exe`, 415 want: `p\a.exe`, 416 }, 417 { 418 name: "with irrelevant PATH", 419 files: []string{`p\a.exe`, `p2\a.exe`}, 420 PATH: []string{".", "p2"}, 421 arg0: `p\a.exe`, 422 want: `p\a.exe`, 423 }, 424 { 425 name: "with slash and no extension", 426 files: []string{`p\a.exe`, `p2\a.exe`}, 427 PATH: []string{".", "p2"}, 428 arg0: `p\a`, 429 want: `p\a.exe`, 430 }, 431 // tests commands, like `a.exe`, with c.Dir set 432 { 433 // should not find a.exe in p, because LookPath(`a.exe`) will fail when 434 // called by Command (before Dir is set), and that error is sticky. 435 name: "not found before Dir", 436 files: []string{`p\a.exe`}, 437 PATH: []string{"."}, 438 dir: `p`, 439 arg0: `a.exe`, 440 want: `p\a.exe`, 441 wantRunErr: exec.ErrNotFound, 442 }, 443 { 444 // LookPath(`a.exe`) will resolve to `.\a.exe`, but prefixing that with 445 // dir `p\a.exe` will refer to a non-existent file 446 name: "resolved before Dir", 447 files: []string{`a.exe`, `p\not_important_file`}, 448 PATH: []string{"."}, 449 dir: `p`, 450 arg0: `a.exe`, 451 want: `a.exe`, 452 wantErrDot: true, 453 wantRunErr: fs.ErrNotExist, 454 }, 455 { 456 // like above, but making test succeed by installing file 457 // in referred destination (so LookPath(`a.exe`) will still 458 // find `.\a.exe`, but we successfully execute `p\a.exe`) 459 name: "relative to Dir", 460 files: []string{`a.exe`, `p\a.exe`}, 461 PATH: []string{"."}, 462 dir: `p`, 463 arg0: `a.exe`, 464 want: `p\a.exe`, 465 wantErrDot: true, 466 }, 467 { 468 // like above, but add PATH in attempt to break the test 469 name: "relative to Dir with extra PATH", 470 files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`}, 471 PATH: []string{".", "p2", "p"}, 472 dir: `p`, 473 arg0: `a.exe`, 474 want: `p\a.exe`, 475 wantErrDot: true, 476 }, 477 { 478 // like above, but use "a" instead of "a.exe" for command 479 name: "relative to Dir with extra PATH and no extension", 480 files: []string{`a.exe`, `p\a.exe`, `p2\a.exe`}, 481 PATH: []string{".", "p2", "p"}, 482 dir: `p`, 483 arg0: `a`, 484 want: `p\a.exe`, 485 wantErrDot: true, 486 }, 487 { 488 // finds `a.exe` in the PATH regardless of Dir because Command resolves the 489 // full path (using LookPath) before Dir is set. 490 name: "from PATH with no match in Dir", 491 files: []string{`p\a.exe`, `p2\a.exe`}, 492 PATH: []string{".", "p2", "p"}, 493 dir: `p`, 494 arg0: `a.exe`, 495 want: `p2\a.exe`, 496 }, 497 // tests commands, like `.\a.exe`, with c.Dir set 498 { 499 // should use dir when command is path, like ".\a.exe" 500 name: "relative to Dir with explicit dot", 501 files: []string{`p\a.exe`}, 502 PATH: []string{"."}, 503 dir: `p`, 504 arg0: `.\a.exe`, 505 want: `p\a.exe`, 506 }, 507 { 508 // like above, but with PATH added in attempt to break it 509 name: "relative to Dir with dot and extra PATH", 510 files: []string{`p\a.exe`, `p2\a.exe`}, 511 PATH: []string{".", "p2"}, 512 dir: `p`, 513 arg0: `.\a.exe`, 514 want: `p\a.exe`, 515 }, 516 { 517 // LookPath(".\a") will fail before Dir is set, and that error is sticky. 518 name: "relative to Dir with dot and extra PATH and no extension", 519 files: []string{`p\a.exe`, `p2\a.exe`}, 520 PATH: []string{".", "p2"}, 521 dir: `p`, 522 arg0: `.\a`, 523 want: `p\a.exe`, 524 }, 525 { 526 // LookPath(".\a") will fail before Dir is set, and that error is sticky. 527 name: "relative to Dir with different extension", 528 files: []string{`a.exe`, `p\a.bat`}, 529 PATH: []string{"."}, 530 dir: `p`, 531 arg0: `.\a`, 532 want: `p\a.bat`, 533 }, 534 } 535 536 func TestCommand(t *testing.T) { 537 // Not parallel: uses Chdir and Setenv. 538 539 // We are using the "printpath" command mode to test exec.Command here, 540 // so we won't be calling helperCommand to resolve it. 541 // That may cause it to appear to be unused. 542 maySkipHelperCommand("printpath") 543 544 for _, tt := range commandTests { 545 t.Run(tt.name, func(t *testing.T) { 546 if tt.PATH == nil { 547 t.Fatalf("test must specify PATH") 548 } 549 550 root := t.TempDir() 551 installProgs(t, root, tt.files) 552 553 pathVar := makePATH(root, tt.PATH) 554 t.Setenv("PATH", pathVar) 555 t.Logf("set PATH=%s", pathVar) 556 557 chdir(t, root) 558 559 cmd := exec.Command(tt.arg0, "printpath") 560 cmd.Dir = filepath.Join(root, tt.dir) 561 if tt.wantErrDot { 562 if errors.Is(cmd.Err, exec.ErrDot) { 563 cmd.Err = nil 564 } else { 565 t.Fatalf("cmd.Err = %v; want ErrDot", cmd.Err) 566 } 567 } 568 569 out, err := cmd.Output() 570 if err != nil { 571 if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 { 572 t.Logf("%v: %v\n%s", cmd, err, ee.Stderr) 573 } else { 574 t.Logf("%v: %v", cmd, err) 575 } 576 if !errors.Is(err, tt.wantRunErr) { 577 t.Errorf("want %v", tt.wantRunErr) 578 } 579 return 580 } 581 582 got := strings.TrimSpace(string(out)) 583 if filepath.IsAbs(got) { 584 got, err = filepath.Rel(root, got) 585 if err != nil { 586 t.Fatal(err) 587 } 588 } 589 if got != tt.want { 590 t.Errorf("\nran %#q\nwant %#q", got, tt.want) 591 } 592 593 gotPath := cmd.Path 594 wantPath := tt.wantPath 595 if wantPath == "" { 596 if strings.Contains(tt.arg0, `\`) { 597 wantPath = tt.arg0 598 } else if tt.wantErrDot { 599 wantPath = strings.TrimPrefix(tt.want, tt.dir+`\`) 600 } else { 601 wantPath = filepath.Join(root, tt.want) 602 } 603 } 604 if gotPath != wantPath { 605 t.Errorf("\ncmd.Path = %#q\nwant %#q", gotPath, wantPath) 606 } 607 }) 608 } 609 } 610 611 func TestAbsCommandWithDoubledExtension(t *testing.T) { 612 t.Parallel() 613 614 // We expect that ".com" is always included in PATHEXT, but it may also be 615 // found in the import path of a Go package. If it is at the root of the 616 // import path, the resulting executable may be named like "example.com.exe". 617 // 618 // Since "example.com" looks like a proper executable name, it is probably ok 619 // for exec.Command to try to run it directly without re-resolving it. 620 // However, exec.LookPath should try a little harder to figure it out. 621 622 comPath := filepath.Join(t.TempDir(), "example.com") 623 batPath := comPath + ".bat" 624 installBat(t, batPath) 625 626 cmd := exec.Command(comPath) 627 out, err := cmd.CombinedOutput() 628 t.Logf("%v: %v\n%s", cmd, err, out) 629 if !errors.Is(err, fs.ErrNotExist) { 630 t.Errorf("Command(%#q).Run: %v\nwant fs.ErrNotExist", comPath, err) 631 } 632 633 resolved, err := exec.LookPath(comPath) 634 if err != nil || resolved != batPath { 635 t.Fatalf("LookPath(%#q) = %v, %v; want %#q, <nil>", comPath, resolved, err, batPath) 636 } 637 }