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