Source file
src/os/exec/lp_windows_test.go
Documentation: os/exec
1
2
3
4
5
6
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
38
39
40
41
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
58
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
70 }
71 if strings.EqualFold(filepath.Ext(f), ".bat") {
72 installBat(t, dstPath)
73 } else {
74 installExe(t, dstPath)
75 }
76 }
77 }
78
79
80
81
82
83
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
108
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
128 files []string
129 PATH []string
130 searchFor string
131 want string
132 wantErr error
133 skipCmdExeCheck bool
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
256
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
278
279
280
281
282 maySkipHelperCommand("printpath")
283
284
285
286
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
326
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
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
372 wantErrDot bool
373 wantRunErr error
374 }
375
376 var commandTests = []commandTest{
377
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
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
432 {
433
434
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
445
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
457
458
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
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
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
489
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
498 {
499
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
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
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
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
538
539
540
541
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
615
616
617
618
619
620
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 }
638
View as plain text