From 765bd84013e35a146359595076b7df1892e56c16 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 9 Oct 2026 15:44:07 +0530 Subject: [PATCH 1/4] feat(cli): offer to install fastlane with Homebrew when it is missing The fastlane APNs key setup stopped with a bare 'needs fastlane' when none was installed. When Homebrew is available it now asks to run brew install fastlane on the terminal and carries on. Without Homebrew, a terminal or a yes, it says how to install fastlane (Homebrew, gem or the project's Gemfile) or to use --provider appwrite. An installed fastlane older than 2.225.0 now gets the matching upgrade command: bundle update fastlane for a project's Bundler fastlane, otherwise brew upgrade or gem update. A project's pinned fastlane and an explicit command are never replaced by an install. --- .../cli/internal/apns/fastlane/adapter.go | 66 ++++++++++- .../internal/apns/fastlane/adapter_test.go | 109 +++++++++++++++++- 2 files changed, 169 insertions(+), 6 deletions(-) diff --git a/templates/cli/internal/apns/fastlane/adapter.go b/templates/cli/internal/apns/fastlane/adapter.go index ee72b6d0d2..a037e38659 100644 --- a/templates/cli/internal/apns/fastlane/adapter.go +++ b/templates/cli/internal/apns/fastlane/adapter.go @@ -40,6 +40,9 @@ type Adapter struct { Fastlane string // Bundle is Bundler's command. Empty means "bundle" from PATH. Bundle string + // Brew is Homebrew's command, offered to install a missing fastlane. + // Empty means "brew" from PATH. + Brew string // WorkDir is the project folder whose Gemfile may list fastlane. Empty // means the current directory. WorkDir string @@ -81,7 +84,13 @@ func (a *Adapter) CreateKey(ctx context.Context, request apns.Request) (apns.Key if err != nil { return apns.Key{}, err } - if err := runner.check(ctx); err != nil { + err = runner.check(ctx) + if errors.Is(err, errNotInstalled) { + if err = a.offerInstall(ctx, request); err == nil { + err = runner.check(ctx) + } + } + if err != nil { return apns.Key{}, err } if runner.gemfile != "" && request.Log != nil { @@ -254,22 +263,69 @@ func (r runner) command(ctx context.Context, arguments ...string) *exec.Cmd { return command } +// errNotInstalled means there is no fastlane on PATH, which installing one +// can fix. +var errNotInstalled = errors.New("fastlane is not installed") + func (r runner) check(ctx context.Context) error { + if r.gemfile == "" && r.name == "fastlane" { + if _, err := exec.LookPath(r.name); err != nil { + return fmt.Errorf("%w: %w. %s", apns.ErrUnavailable, errNotInstalled, installHint) + } + } output, err := r.command(ctx, "--version").Output() if err != nil { if r.gemfile != "" { return fmt.Errorf("%w: bundle exec fastlane failed with %s. Run bundle install there first", apns.ErrUnavailable, r.gemfile) } - return fmt.Errorf("%w: the fastlane setup needs fastlane %s or later installed (https://docs.fastlane.tools)", apns.ErrUnavailable, versionString(minimumVersion)) + return fmt.Errorf("%w: %s --version failed: %v", apns.ErrUnavailable, r.name, err) + } + upgrade := "brew upgrade fastlane, or gem update fastlane, depending on how it was installed" + if r.gemfile != "" { + upgrade = "bundle update fastlane in " + filepath.Dir(r.gemfile) + } + + return supportedVersion(string(output), upgrade) +} + +const installHint = "Install it with brew install fastlane (Homebrew) or gem install fastlane, " + + "or add gem \"fastlane\" to your project's Gemfile and run bundle install. " + + "Or use --provider appwrite, which needs nothing installed" + +// offerInstall asks to install fastlane with Homebrew, when there is one, and +// runs it on the terminal. Without Homebrew, a terminal or a yes, it returns +// how to install fastlane instead. +func (a *Adapter) offerInstall(ctx context.Context, request apns.Request) error { + notInstalled := fmt.Errorf("%w: %w. %s", apns.ErrUnavailable, errNotInstalled, installHint) + brew := a.Brew + if brew == "" { + brew = "brew" + } + if _, err := exec.LookPath(brew); err != nil || request.Asker == nil { + return notInstalled + } + choice, err := request.Asker.Choose("fastlane is not installed. Install it with Homebrew now?", + []string{"Yes, run brew install fastlane", "No"}) + if err != nil || choice != 0 { + return notInstalled } - return supportedVersion(string(output)) + if request.Log != nil { + request.Log("Running brew install fastlane ...") + } + command := exec.CommandContext(ctx, brew, "install", "fastlane") + command.Stdin, command.Stdout, command.Stderr = a.terminal() + if err := command.Run(); err != nil { + return fmt.Errorf("%w: brew install fastlane failed: %v. %s", apns.ErrUnavailable, err, installHint) + } + + return nil } var versionPattern = regexp.MustCompile(`fastlane (\d+)\.(\d+)\.(\d+)`) -func supportedVersion(output string) error { +func supportedVersion(output, upgrade string) error { match := versionPattern.FindStringSubmatch(output) if match == nil { return fmt.Errorf("%w: could not read the fastlane version", apns.ErrUnavailable) @@ -281,7 +337,7 @@ func supportedVersion(output string) error { for index := range version { if version[index] != minimumVersion[index] { if version[index] < minimumVersion[index] { - return fmt.Errorf("%w: the fastlane setup needs fastlane %s or later, found %s. Run fastlane update_fastlane", apns.ErrUnavailable, versionString(minimumVersion), versionString(version)) + return fmt.Errorf("%w: the fastlane setup needs fastlane %s or later, found %s. Upgrade it with %s", apns.ErrUnavailable, versionString(minimumVersion), versionString(version), upgrade) } break diff --git a/templates/cli/internal/apns/fastlane/adapter_test.go b/templates/cli/internal/apns/fastlane/adapter_test.go index 8a95dafa4a..e539036341 100644 --- a/templates/cli/internal/apns/fastlane/adapter_test.go +++ b/templates/cli/internal/apns/fastlane/adapter_test.go @@ -22,7 +22,7 @@ func TestSupportedVersion(t *testing.T) { "fastlane 1.300.0": false, "something else": false, } { - err := supportedVersion(output) + err := supportedVersion(output, "brew upgrade fastlane") if (err == nil) != supported { t.Errorf("supportedVersion(%q) = %v", output, err) } @@ -251,3 +251,110 @@ func TestCreateKeyWillNotBypassAPinnedFastlane(t *testing.T) { t.Fatalf("err = %v", err) } } + +// choosingAsker answers every choice with choice and records the questions. +type choosingAsker struct { + choice int + asked []string +} + +func (c *choosingAsker) Ask(question string, secret bool) (string, error) { + return "", errors.New("not expected: " + question) +} + +func (c *choosingAsker) Choose(question string, options []string) (int, error) { + c.asked = append(c.asked, question) + + return c.choice, nil +} + +// withoutFastlane leaves PATH with only bin, so fastlane is not found unless +// something puts it there. +func withoutFastlane(t *testing.T) string { + t.Helper() + if runtime.GOOS == "windows" { + t.Skip("shell stand-ins") + } + bin := t.TempDir() + t.Setenv("PATH", bin) + + return bin +} + +// fakeBrew writes a brew stand-in into bin that logs its arguments and, for +// install fastlane, copies fastlane into bin. +func fakeBrew(t *testing.T, bin, fastlane string) string { + t.Helper() + log := filepath.Join(t.TempDir(), "brew.log") + script := `#!/bin/sh +echo "brew $*" >> "` + log + `" +[ "$1 $2" = "install fastlane" ] && /bin/cp "` + fastlane + `" "` + bin + `/fastlane" +` + if err := os.WriteFile(filepath.Join(bin, "brew"), []byte(script), 0o700); err != nil { + t.Fatal(err) + } + + return log +} + +func TestCreateKeyExplainsHowToInstallFastlane(t *testing.T) { + withoutFastlane(t) + request := apns.Request{Name: "Appwrite Push", Environment: apns.EnvironmentAll, Asker: &choosingAsker{}} + + _, err := (&Adapter{}).CreateKey(context.Background(), request) + if !errors.Is(err, apns.ErrUnavailable) { + t.Fatalf("err = %v", err) + } + for _, part := range []string{"fastlane is not installed", "brew install fastlane", "gem install fastlane", "--provider appwrite"} { + if !strings.Contains(err.Error(), part) { + t.Errorf("message is missing %q: %v", part, err) + } + } +} + +func TestCreateKeyInstallsFastlaneWithHomebrew(t *testing.T) { + fastlane, _ := fakeFastlane(t, "2.228.0", `{"keyId":"KEY1234567","teamId":"ABCDE12345","p8":"pem"}`) + bin := withoutFastlane(t) + log := fakeBrew(t, bin, fastlane) + asker := &choosingAsker{choice: 0} + var logged []string + request := apns.Request{ + Name: "Appwrite Push", Environment: apns.EnvironmentAll, AppleID: "dev@example.com", Password: "secret", Asker: asker, + Log: func(format string, args ...any) { logged = append(logged, format) }, + } + + key, err := (&Adapter{Stdout: &strings.Builder{}, Stderr: &strings.Builder{}}).CreateKey(context.Background(), request) + if err != nil || key.KeyID != "KEY1234567" { + t.Fatalf("key = %+v, %v", key, err) + } + if strings.Join(asker.asked, "|") != "fastlane is not installed. Install it with Homebrew now?" { + t.Errorf("asked %v", asker.asked) + } + if calls, _ := os.ReadFile(log); strings.TrimSpace(string(calls)) != "brew install fastlane" { + t.Errorf("brew calls = %q", calls) + } + if len(logged) != 1 || logged[0] != "Running brew install fastlane ..." { + t.Errorf("logged %v", logged) + } +} + +func TestCreateKeyDoesNotInstallFastlaneWhenDeclined(t *testing.T) { + fastlane, _ := fakeFastlane(t, "2.228.0", `{}`) + bin := withoutFastlane(t) + log := fakeBrew(t, bin, fastlane) + + _, err := (&Adapter{}).CreateKey(context.Background(), apns.Request{Name: "Appwrite Push", Environment: apns.EnvironmentAll, Asker: &choosingAsker{choice: 1}}) + if !errors.Is(err, apns.ErrUnavailable) || !strings.Contains(err.Error(), "brew install fastlane") { + t.Fatalf("err = %v", err) + } + if _, err := os.Stat(log); err == nil { + t.Error("ran brew after the offer was declined") + } +} + +func TestOldFastlaneSaysHowToUpgrade(t *testing.T) { + if err := supportedVersion("fastlane 2.220.0", "bundle update fastlane in /app"); err == nil || + !strings.Contains(err.Error(), "found 2.220.0. Upgrade it with bundle update fastlane in /app") { + t.Errorf("err = %v", err) + } +} From f0455a1ec3e833da0da160dabc0ed9caa269d98e Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 9 Oct 2026 15:59:45 +0530 Subject: [PATCH 2/4] fix(cli): never install over a configured fastlane, and fix the Bundler hint A fastlane command set on the adapter was offered a Homebrew install when it was missing, which could replace a deliberate choice; the offer is now only for fastlane from PATH. The too-old message for a project's Bundler fastlane said 'bundle update fastlane in ', which Bundler reads as gem names; it now says 'cd && bundle update fastlane'. --- .../cli/internal/apns/fastlane/adapter.go | 6 ++- .../internal/apns/fastlane/adapter_test.go | 42 ++++++++++++++++++- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/templates/cli/internal/apns/fastlane/adapter.go b/templates/cli/internal/apns/fastlane/adapter.go index a037e38659..0d822cb4b5 100644 --- a/templates/cli/internal/apns/fastlane/adapter.go +++ b/templates/cli/internal/apns/fastlane/adapter.go @@ -85,7 +85,9 @@ func (a *Adapter) CreateKey(ctx context.Context, request apns.Request) (apns.Key return apns.Key{}, err } err = runner.check(ctx) - if errors.Is(err, errNotInstalled) { + // Installing is only offered for fastlane from PATH: a configured command + // is never replaced. + if errors.Is(err, errNotInstalled) && a.Fastlane == "" { if err = a.offerInstall(ctx, request); err == nil { err = runner.check(ctx) } @@ -283,7 +285,7 @@ func (r runner) check(ctx context.Context) error { } upgrade := "brew upgrade fastlane, or gem update fastlane, depending on how it was installed" if r.gemfile != "" { - upgrade = "bundle update fastlane in " + filepath.Dir(r.gemfile) + upgrade = "cd " + filepath.Dir(r.gemfile) + " && bundle update fastlane" } return supportedVersion(string(output), upgrade) diff --git a/templates/cli/internal/apns/fastlane/adapter_test.go b/templates/cli/internal/apns/fastlane/adapter_test.go index e539036341..a9cb5f83d7 100644 --- a/templates/cli/internal/apns/fastlane/adapter_test.go +++ b/templates/cli/internal/apns/fastlane/adapter_test.go @@ -353,8 +353,46 @@ func TestCreateKeyDoesNotInstallFastlaneWhenDeclined(t *testing.T) { } func TestOldFastlaneSaysHowToUpgrade(t *testing.T) { - if err := supportedVersion("fastlane 2.220.0", "bundle update fastlane in /app"); err == nil || - !strings.Contains(err.Error(), "found 2.220.0. Upgrade it with bundle update fastlane in /app") { + if err := supportedVersion("fastlane 2.220.0", "cd /app && bundle update fastlane"); err == nil || + !strings.Contains(err.Error(), "found 2.220.0. Upgrade it with cd /app && bundle update fastlane") { + t.Errorf("err = %v", err) + } +} + +func TestAConfiguredFastlaneIsNeverInstalled(t *testing.T) { + fastlane, _ := fakeFastlane(t, "2.228.0", `{}`) + bin := withoutFastlane(t) + log := fakeBrew(t, bin, fastlane) + asker := &choosingAsker{choice: 0} + + _, err := (&Adapter{Fastlane: "fastlane"}).CreateKey(context.Background(), apns.Request{Name: "Appwrite Push", Environment: apns.EnvironmentAll, Asker: asker}) + if !errors.Is(err, apns.ErrUnavailable) { + t.Fatalf("err = %v", err) + } + if len(asker.asked) != 0 { + t.Errorf("offered to install: %v", asker.asked) + } + if _, err := os.Stat(log); err == nil { + t.Error("ran brew for a configured command") + } +} + +func TestTheBundlerUpgradeCommandRuns(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("shell stand-in") + } + project := t.TempDir() + gemfile := filepath.Join(project, "Gemfile") + if err := os.WriteFile(gemfile, []byte("gem \"fastlane\"\n"), 0o600); err != nil { + t.Fatal(err) + } + bundle := filepath.Join(t.TempDir(), "bundle") + if err := os.WriteFile(bundle, []byte("#!/bin/sh\necho \"fastlane 2.220.0\"\n"), 0o700); err != nil { + t.Fatal(err) + } + + _, err := (&Adapter{Bundle: bundle, WorkDir: project}).CreateKey(context.Background(), apns.Request{Name: "Appwrite Push", Environment: apns.EnvironmentAll, AppleID: "dev@example.com", Password: "secret"}) + if err == nil || !strings.Contains(err.Error(), "Upgrade it with cd "+project+" && bundle update fastlane") { t.Errorf("err = %v", err) } } From 86be24b0be85bf754d905e7a3e9aed58e27f77d4 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 9 Oct 2026 16:04:28 +0530 Subject: [PATCH 3/4] fix(cli): quote the project folder in the Bundler upgrade hint cd && bundle update fastlane broke for folders with spaces: the shell split the path. The folder is now quoted for the shell people paste into, single quotes on Unix (with a single quote in the name escaped) and double quotes on Windows, and left as is when it has only safe characters. --- .../cli/internal/apns/fastlane/adapter.go | 22 ++++++++++++- .../internal/apns/fastlane/adapter_test.go | 33 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/templates/cli/internal/apns/fastlane/adapter.go b/templates/cli/internal/apns/fastlane/adapter.go index 0d822cb4b5..9b8c689ffd 100644 --- a/templates/cli/internal/apns/fastlane/adapter.go +++ b/templates/cli/internal/apns/fastlane/adapter.go @@ -19,6 +19,7 @@ import ( "os/exec" "path/filepath" "regexp" + "runtime" "strconv" "strings" @@ -285,12 +286,31 @@ func (r runner) check(ctx context.Context) error { } upgrade := "brew upgrade fastlane, or gem update fastlane, depending on how it was installed" if r.gemfile != "" { - upgrade = "cd " + filepath.Dir(r.gemfile) + " && bundle update fastlane" + upgrade = "cd " + shellQuote(filepath.Dir(r.gemfile), runtime.GOOS) + " && bundle update fastlane" } return supportedVersion(string(output), upgrade) } +// shellQuote quotes path for the shell people paste a command into: single +// quotes on Unix, with any single quote closed, escaped and reopened, and +// double quotes on Windows, where paths cannot contain one. A path of only +// safe characters is left as it is. +func shellQuote(path, goos string) string { + safe := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/._-" + if goos == "windows" { + safe += ":\\" + } + if path != "" && strings.Trim(path, safe) == "" { + return path + } + if goos == "windows" { + return `"` + path + `"` + } + + return "'" + strings.ReplaceAll(path, "'", `'\''`) + "'" +} + const installHint = "Install it with brew install fastlane (Homebrew) or gem install fastlane, " + "or add gem \"fastlane\" to your project's Gemfile and run bundle install. " + "Or use --provider appwrite, which needs nothing installed" diff --git a/templates/cli/internal/apns/fastlane/adapter_test.go b/templates/cli/internal/apns/fastlane/adapter_test.go index a9cb5f83d7..36a09aa379 100644 --- a/templates/cli/internal/apns/fastlane/adapter_test.go +++ b/templates/cli/internal/apns/fastlane/adapter_test.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "os/exec" "path/filepath" "runtime" "strings" @@ -396,3 +397,35 @@ func TestTheBundlerUpgradeCommandRuns(t *testing.T) { t.Errorf("err = %v", err) } } + +func TestShellQuote(t *testing.T) { + for _, test := range []struct{ path, goos, want string }{ + {"/Users/dev/app", "darwin", "/Users/dev/app"}, + {"/Users/dev/My App", "darwin", "'/Users/dev/My App'"}, + {"/Users/dev/it's", "linux", `'/Users/dev/it'\''s'`}, + {`/Users/dev/back\slash`, "linux", `'/Users/dev/back\slash'`}, + {`C:\Users\dev\app`, "windows", `C:\Users\dev\app`}, + {`C:\Users\dev\My App`, "windows", `"C:\Users\dev\My App"`}, + } { + if got := shellQuote(test.path, test.goos); got != test.want { + t.Errorf("shellQuote(%q, %s) = %s, want %s", test.path, test.goos, got, test.want) + } + } +} + +// The quoted command must work when pasted into a shell, for a folder whose +// name has spaces and a quote. +func TestTheQuotedCdRunsInAShell(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("POSIX shell") + } + folder := filepath.Join(t.TempDir(), "My Project's App") + if err := os.Mkdir(folder, 0o700); err != nil { + t.Fatal(err) + } + output, err := exec.Command("/bin/sh", "-c", "cd "+shellQuote(folder, runtime.GOOS)+" && pwd -P").Output() + resolved, _ := filepath.EvalSymlinks(folder) + if err != nil || strings.TrimSpace(string(output)) != resolved { + t.Errorf("cd landed in %q, %v; want %q", output, err, resolved) + } +} From 941fda72a05d678f616cd5cefda71a7c28762b1f Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 9 Oct 2026 16:07:54 +0530 Subject: [PATCH 4/4] fix(cli): name the folder instead of quoting it on Windows Double quotes are not safe on Windows: PowerShell expands $name and $() inside them, so a folder name could change the pasted command or run an expression, cmd expands %, and Windows PowerShell has no &&. No single quoting works in every Windows shell, so the Bundler upgrade hint there names the folder to run bundle update fastlane from. Unix keeps the single-quoted cd command. --- .../cli/internal/apns/fastlane/adapter.go | 29 +++++++++++-------- .../internal/apns/fastlane/adapter_test.go | 22 +++++++------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/templates/cli/internal/apns/fastlane/adapter.go b/templates/cli/internal/apns/fastlane/adapter.go index 9b8c689ffd..5dbb2b0c75 100644 --- a/templates/cli/internal/apns/fastlane/adapter.go +++ b/templates/cli/internal/apns/fastlane/adapter.go @@ -286,27 +286,32 @@ func (r runner) check(ctx context.Context) error { } upgrade := "brew upgrade fastlane, or gem update fastlane, depending on how it was installed" if r.gemfile != "" { - upgrade = "cd " + shellQuote(filepath.Dir(r.gemfile), runtime.GOOS) + " && bundle update fastlane" + upgrade = bundlerUpgrade(filepath.Dir(r.gemfile), runtime.GOOS) } return supportedVersion(string(output), upgrade) } -// shellQuote quotes path for the shell people paste a command into: single -// quotes on Unix, with any single quote closed, escaped and reopened, and -// double quotes on Windows, where paths cannot contain one. A path of only -// safe characters is left as it is. -func shellQuote(path, goos string) string { - safe := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/._-" +// bundlerUpgrade says how to upgrade a project's Bundler fastlane. On Unix +// it is one command to paste, with the folder quoted for a POSIX shell. On +// Windows no quoting is safe in every shell (PowerShell expands $ inside +// double quotes, cmd expands %, and Windows PowerShell has no &&), so the +// folder is named rather than put in a command. +func bundlerUpgrade(folder, goos string) string { if goos == "windows" { - safe += ":\\" + return "bundle update fastlane, run from the folder " + folder } - if path != "" && strings.Trim(path, safe) == "" { + + return "cd " + shellQuote(folder) + " && bundle update fastlane" +} + +// shellQuote quotes path for a POSIX shell: in single quotes, with any single +// quote closed, escaped and reopened. A path of only safe characters is left +// as it is. +func shellQuote(path string) string { + if path != "" && strings.Trim(path, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/._-") == "" { return path } - if goos == "windows" { - return `"` + path + `"` - } return "'" + strings.ReplaceAll(path, "'", `'\''`) + "'" } diff --git a/templates/cli/internal/apns/fastlane/adapter_test.go b/templates/cli/internal/apns/fastlane/adapter_test.go index 36a09aa379..f6c45457a2 100644 --- a/templates/cli/internal/apns/fastlane/adapter_test.go +++ b/templates/cli/internal/apns/fastlane/adapter_test.go @@ -398,17 +398,17 @@ func TestTheBundlerUpgradeCommandRuns(t *testing.T) { } } -func TestShellQuote(t *testing.T) { - for _, test := range []struct{ path, goos, want string }{ - {"/Users/dev/app", "darwin", "/Users/dev/app"}, - {"/Users/dev/My App", "darwin", "'/Users/dev/My App'"}, - {"/Users/dev/it's", "linux", `'/Users/dev/it'\''s'`}, - {`/Users/dev/back\slash`, "linux", `'/Users/dev/back\slash'`}, - {`C:\Users\dev\app`, "windows", `C:\Users\dev\app`}, - {`C:\Users\dev\My App`, "windows", `"C:\Users\dev\My App"`}, +func TestBundlerUpgrade(t *testing.T) { + for _, test := range []struct{ folder, goos, want string }{ + {"/Users/dev/app", "darwin", "cd /Users/dev/app && bundle update fastlane"}, + {"/Users/dev/My App", "darwin", "cd '/Users/dev/My App' && bundle update fastlane"}, + {"/Users/dev/it's", "linux", `cd '/Users/dev/it'\''s' && bundle update fastlane`}, + {`/Users/dev/back\slash`, "linux", `cd '/Users/dev/back\slash' && bundle update fastlane`}, + {`/Users/dev/$(whoami)`, "linux", `cd '/Users/dev/$(whoami)' && bundle update fastlane`}, + {`C:\Users\dev\$(Get-Date) App`, "windows", `bundle update fastlane, run from the folder C:\Users\dev\$(Get-Date) App`}, } { - if got := shellQuote(test.path, test.goos); got != test.want { - t.Errorf("shellQuote(%q, %s) = %s, want %s", test.path, test.goos, got, test.want) + if got := bundlerUpgrade(test.folder, test.goos); got != test.want { + t.Errorf("bundlerUpgrade(%q, %s) = %s, want %s", test.folder, test.goos, got, test.want) } } } @@ -423,7 +423,7 @@ func TestTheQuotedCdRunsInAShell(t *testing.T) { if err := os.Mkdir(folder, 0o700); err != nil { t.Fatal(err) } - output, err := exec.Command("/bin/sh", "-c", "cd "+shellQuote(folder, runtime.GOOS)+" && pwd -P").Output() + output, err := exec.Command("/bin/sh", "-c", "cd "+shellQuote(folder)+" && pwd -P").Output() resolved, _ := filepath.EvalSymlinks(folder) if err != nil || strings.TrimSpace(string(output)) != resolved { t.Errorf("cd landed in %q, %v; want %q", output, err, resolved)