| 1 | package sandbox |
| 2 | |
| 3 | import ( |
| 4 | "os" |
| 5 | "path/filepath" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | ) |
| 9 | |
| 10 | func TestConfiguredShellPathCompatibilityAndCacheIdentity(t *testing.T) { |
| 11 | exists := func(string) bool { return true } |
| 12 | noWSL := func(string) bool { return false } |
| 13 | for _, goos := range []string{"darwin", "linux"} { |
| 14 | if got := configuredShellPathForPreference(goos, "powershell", "/portable/pwsh", exists, noWSL); got != "/portable/pwsh" { |
| 15 | t.Fatalf("%s: legacy PowerShell preference lost its configured pwsh: %q", goos, got) |
| 16 | } |
| 17 | if shellInventoryKey(goos, "bash", "/portable/A/bash") == shellInventoryKey(goos, "bash", "/portable/a/bash") { |
| 18 | t.Fatalf("%s: different configured executables share a capability snapshot", goos) |
| 19 | } |
| 20 | } |
| 21 | if shellInventoryKey("windows", "PWSH", `C:\Portable\pwsh.exe`) != shellInventoryKey("windows", "pwsh", `c:\portable\PWSH.EXE`) { |
| 22 | t.Fatal("Windows snapshot identity must remain case-insensitive") |
| 23 | } |
| 24 | } |
| 25 | |
| 26 | func TestConfiguredShellSnapshotUsesConfiguredExecutable(t *testing.T) { |
| 27 | for _, goos := range []string{"windows", "darwin", "linux"} { |
| 28 | t.Run(goos, func(t *testing.T) { |
| 29 | prefer, binary, id := "bash", "bash", ShellCapabilityBash |
| 30 | if goos == "windows" { |
| 31 | prefer, binary, id = "pwsh", "pwsh.exe", ShellCapabilityPwsh |
| 32 | } |
| 33 | configured := filepath.Join(t.TempDir(), binary) |
| 34 | if err := os.WriteFile(configured, []byte("#!/bin/sh\nexit 0\n"), 0o700); err != nil { |
| 35 | t.Fatal(err) |
| 36 | } |
| 37 | // A real snapshot must retain config provenance, not only manually |
| 38 | // constructed unit-test snapshots. Non-Windows Bash probes are no-ops. |
| 39 | snap := buildShellSnapshot(goos, prefer, configured) |
| 40 | // On Windows, POSIX fixtures cannot execute a shebang; the native |
| 41 | // configured PowerShell branch above needs no such probe. |
| 42 | if goos != "windows" { |
| 43 | snap.probeFunc = func(string) bool { return true } |
| 44 | snap.probeCache = map[string]bool{} |
| 45 | snap.caps = unixShellCapabilities(snap) |
| 46 | } |
| 47 | sh := resolveShell(prefer, configured, nil, goos, snap.lookPath, snap.exists, snap.bashCands, snap.psCands, snap.probe, snap.isWSL) |
| 48 | if sh.Path != configured { |
| 49 | t.Fatalf("runtime did not select config: %+v", sh) |
| 50 | } |
| 51 | for _, cap := range snap.caps { |
| 52 | if cap.ID == id && (!cap.Available || cap.Path != configured || cap.Source != ShellSourceConfig) { |
| 53 | t.Fatalf("config was lost during discovery: %+v", cap) |
| 54 | } |
| 55 | } |
| 56 | }) |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | func TestWSLBashAliasesExcluded(t *testing.T) { |
| 61 | const gitBash = `D:\PortableGit\bin\bash.exe` |
| 62 | isWSL := func(p string) bool { return isWindowsWSLBashPath(p, `C:\Windows`) } |
| 63 | for _, alias := range []string{ |
| 64 | `C:\Windows\System32\bash.exe`, |
| 65 | `C:\Users\tester\AppData\Local\Microsoft\WindowsApps\bash.exe`, |
| 66 | `C:/Users/tester/AppData/Local/MICROSOFT/WindowsApps/BASH.EXE`, |
| 67 | `\\?\C:\Users\tester\AppData\Local\Microsoft\WindowsApps\bash.exe`, |
| 68 | `C:\Users\tester\AppData\Local\Microsoft\WindowsApps\MicrosoftCorporationII.WindowsSubsystemForLinux_8wekyb3d8bbwe\bash.exe`, |
| 69 | } { |
| 70 | t.Run(alias, func(t *testing.T) { |
| 71 | if !isWSL(alias) { |
| 72 | t.Fatal("WSL launcher was accepted as native Bash") |
| 73 | } |
| 74 | lookup := fakeLookPath(map[string]string{"bash": alias}) |
| 75 | for _, configured := range []string{"", alias} { |
| 76 | sh := resolveShell("bash", configured, nil, "windows", lookup, func(string) bool { return true }, []string{gitBash}, nil, func(string) bool { return true }, isWSL) |
| 77 | if sh.Path != gitBash { |
| 78 | t.Fatalf("configured=%q resolved=%+v; must skip even a healthy WSL alias", configured, sh) |
| 79 | } |
| 80 | } |
| 81 | snap := &shellSnapshot{lookPath: lookup, exists: func(string) bool { return true }, isWSL: isWSL, |
| 82 | bashCands: []string{gitBash}, probeFunc: func(string) bool { return true }, probeCache: map[string]bool{}} |
| 83 | if cap := windowsShellCapabilities(snap)[0]; !cap.Available || cap.Path != gitBash { |
| 84 | t.Fatalf("capability accepted WSL: %+v", cap) |
| 85 | } |
| 86 | }) |
| 87 | } |
| 88 | if !isWindowsWSLBashPath(`C:\Users\tester\AppData\Local\Microsoft\WindowsApps\bash.exe`, "") { |
| 89 | t.Fatal("alias exclusion must not depend on SystemRoot") |
| 90 | } |
| 91 | for _, native := range []string{"", gitBash, `C:\WindowsPortable\Git\bin\bash.exe`, `C:\Users\tester\Microsoft\WindowsAppsOther\bash.exe`} { |
| 92 | if isWSL(native) { |
| 93 | t.Fatalf("native path incorrectly excluded: %q", native) |
| 94 | } |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | func TestPowerShellPreferenceAndInventoryUseCompatibleConfiguredPath(t *testing.T) { |
| 99 | const ps = `C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe` |
| 100 | const pwsh = `C:\Program Files\PowerShell\7\pwsh.exe` |
| 101 | const portablePS = `D:\PortablePowerShell\powershell.exe` |
| 102 | const portablePwsh = `D:\PortablePowerShell\pwsh.exe` |
| 103 | const wrapper = `D:\Tools\ps-wrapper.exe` |
| 104 | for _, tc := range []struct { |
| 105 | name, prefer, configured, want string |
| 106 | installed bool |
| 107 | }{ |
| 108 | {"switch to Windows PowerShell", "powershell", portablePwsh, ps, true}, |
| 109 | {"switch to PowerShell 7", "pwsh", portablePS, pwsh, true}, |
| 110 | {"portable PS5", "powershell", portablePS, portablePS, false}, |
| 111 | {"portable PS7", "pwsh", portablePwsh, portablePwsh, false}, |
| 112 | {"config before standard", "pwsh", portablePwsh, portablePwsh, true}, |
| 113 | {"auto portable PS7", "auto", portablePwsh, portablePwsh, false}, |
| 114 | {"auto portable PS5", "auto", portablePS, portablePS, false}, |
| 115 | {"case insensitive", " PWSH ", portablePwsh, portablePwsh, false}, |
| 116 | {"intentional wrapper", "powershell", wrapper, wrapper, false}, |
| 117 | } { |
| 118 | t.Run(tc.name, func(t *testing.T) { |
| 119 | paths := map[string]string{} |
| 120 | var candidates []string |
| 121 | if tc.installed { |
| 122 | paths["powershell"], paths["pwsh"] = ps, pwsh |
| 123 | candidates = []string{pwsh, ps} |
| 124 | } |
| 125 | lookup := fakeLookPath(paths) |
| 126 | exists := func(p string) bool { return p == tc.configured || tc.installed && (p == ps || p == pwsh) } |
| 127 | noWSL := func(string) bool { return false } |
| 128 | sh := resolveShell(tc.prefer, tc.configured, nil, "windows", lookup, exists, nil, candidates, func(string) bool { return true }, noWSL) |
| 129 | if sh.Path != tc.want { |
| 130 | t.Fatalf("resolved=%+v, want %q", sh, tc.want) |
| 131 | } |
| 132 | snap := &shellSnapshot{prefer: tc.prefer, configPath: tc.configured, lookPath: lookup, exists: exists, isWSL: noWSL, psCands: candidates, probeCache: map[string]bool{}} |
| 133 | id := ShellCapabilityPowerShell |
| 134 | if sh.SupportsChaining() { |
| 135 | id = ShellCapabilityPwsh |
| 136 | } |
| 137 | for _, cap := range windowsShellCapabilities(snap) { |
| 138 | if cap.ID != id { |
| 139 | continue |
| 140 | } |
| 141 | if !cap.Available || cap.Path != tc.want { |
| 142 | t.Fatalf("runtime=%+v, capability=%+v", sh, cap) |
| 143 | } |
| 144 | if tc.want == tc.configured && cap.Source != ShellSourceConfig { |
| 145 | t.Fatalf("custom path source=%q", cap.Source) |
| 146 | } |
| 147 | } |
| 148 | if strings.TrimSpace(tc.prefer) != "auto" && tc.want != tc.configured && configuredShellPathForPreference("windows", tc.prefer, tc.configured, exists, noWSL) != "" { |
| 149 | t.Fatal("shared path consumer retained the wrong PowerShell version") |
| 150 | } |
| 151 | }) |
| 152 | } |
| 153 | } |
| 154 |