Source file src/os/exec/lp_windows_test.go

     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  }
   638  

View as plain text