Skip to content

Commit 1370c97

Browse files
jerryjrxieclaude
andcommitted
fix: address PR review feedback for batch install
- Show progress status before batch install starts (PrintLine instead of staying at 0% until completion) - Remove dead code: installCaskWithProgress and printBrewOutput - Capture brew output for specific error messages per failed package (extractPackageError) instead of generic "install failed" - Add meaningful assertions to TestInstallWithProgress_BatchMode verifying batch command format in dry-run output - Add TestExtractPackageError for the new helper Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b176f99 commit 1370c97

2 files changed

Lines changed: 98 additions & 49 deletions

File tree

internal/brew/brew.go

Lines changed: 35 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -259,13 +259,12 @@ func InstallWithProgress(cliPkgs, caskPkgs []string, dryRun bool) (installedForm
259259
var allFailed []failedJob
260260

261261
if len(newCli) > 0 {
262-
ui.Info(fmt.Sprintf("Installing %d CLI packages...", len(newCli)))
262+
progress.PrintLine(" Installing %d CLI packages via brew install...", len(newCli))
263263

264264
args := append([]string{"install"}, newCli...)
265265
cmd := brewInstallCmd(args...)
266-
cmd.Stdout = os.Stdout
267-
cmd.Stderr = os.Stderr
268-
cmdErr := cmd.Run()
266+
cmdOutput, cmdErr := cmd.CombinedOutput()
267+
cmdOutputStr := string(cmdOutput)
269268

270269
// Re-check installed packages to determine actual success
271270
postFormulae, _, postErr := GetInstalledPackages()
@@ -278,7 +277,7 @@ func InstallWithProgress(cliPkgs, caskPkgs []string, dryRun bool) (installedForm
278277
} else {
279278
allFailed = append(allFailed, failedJob{
280279
installJob: installJob{name: pkg, isCask: false},
281-
errMsg: "install failed",
280+
errMsg: parseBrewError(cmdOutputStr),
282281
})
283282
}
284283
}
@@ -292,26 +291,25 @@ func InstallWithProgress(cliPkgs, caskPkgs []string, dryRun bool) (installedForm
292291
} else {
293292
allFailed = append(allFailed, failedJob{
294293
installJob: installJob{name: pkg, isCask: false},
295-
errMsg: "install failed",
294+
errMsg: extractPackageError(cmdOutputStr, pkg),
296295
})
297296
}
298297
}
299298
}
300299
}
301300

302301
if len(newCask) > 0 {
303-
ui.Info(fmt.Sprintf("Installing %d GUI apps...", len(newCask)))
302+
progress.PrintLine(" Installing %d GUI apps via brew install --cask...", len(newCask))
304303

305304
args := append([]string{"install", "--cask"}, newCask...)
306305
cmd := brewInstallCmd(args...)
307-
cmd.Stdout = os.Stdout
308-
cmd.Stderr = os.Stderr
309306
// Open TTY for password prompts
310307
tty, opened := system.OpenTTY()
311308
if opened {
312309
cmd.Stdin = tty
313310
}
314-
cmdErr := cmd.Run()
311+
cmdOutput, cmdErr := cmd.CombinedOutput()
312+
cmdOutputStr := string(cmdOutput)
315313
if opened {
316314
tty.Close()
317315
}
@@ -327,7 +325,7 @@ func InstallWithProgress(cliPkgs, caskPkgs []string, dryRun bool) (installedForm
327325
} else {
328326
allFailed = append(allFailed, failedJob{
329327
installJob: installJob{name: pkg, isCask: true},
330-
errMsg: "install failed",
328+
errMsg: parseBrewError(cmdOutputStr),
331329
})
332330
}
333331
}
@@ -341,7 +339,7 @@ func InstallWithProgress(cliPkgs, caskPkgs []string, dryRun bool) (installedForm
341339
} else {
342340
allFailed = append(allFailed, failedJob{
343341
installJob: installJob{name: pkg, isCask: true},
344-
errMsg: "install failed",
342+
errMsg: extractPackageError(cmdOutputStr, pkg),
345343
})
346344
}
347345
}
@@ -418,36 +416,6 @@ type failedJob struct {
418416
errMsg string
419417
}
420418

421-
func installCaskWithProgress(pkg string, progress *ui.StickyProgress) string {
422-
progress.PauseForInteractive()
423-
424-
cmd := brewInstallCmd("install", "--cask", pkg)
425-
tty, opened := system.OpenTTY()
426-
if opened {
427-
defer tty.Close()
428-
}
429-
cmd.Stdin = tty
430-
cmd.Stdout = os.Stdout
431-
cmd.Stderr = os.Stderr
432-
err := cmd.Run()
433-
434-
progress.ResumeAfterInteractive()
435-
436-
if err != nil {
437-
return "install failed"
438-
}
439-
return ""
440-
}
441-
442-
func printBrewOutput(output string, progress *ui.StickyProgress) {
443-
for _, line := range strings.Split(strings.TrimSpace(output), "\n") {
444-
line = strings.TrimSpace(line)
445-
if line != "" {
446-
progress.PrintLine(" %s", line)
447-
}
448-
}
449-
}
450-
451419
func brewInstallCmd(args ...string) *exec.Cmd {
452420
cmd := exec.Command("brew", args...)
453421
cmd.Env = append(os.Environ(), "HOMEBREW_NO_AUTO_UPDATE=1")
@@ -585,6 +553,31 @@ func parseBrewError(output string) string {
585553
}
586554
}
587555

556+
// extractPackageError tries to find an error message specific to pkg in the
557+
// combined brew output. Falls back to parseBrewError on the full output.
558+
func extractPackageError(output, pkg string) string {
559+
// Scan for lines mentioning the package name near an error indicator.
560+
lowerPkg := strings.ToLower(pkg)
561+
for _, line := range strings.Split(output, "\n") {
562+
lower := strings.ToLower(line)
563+
if strings.Contains(lower, lowerPkg) && strings.Contains(lower, "error") {
564+
line = strings.TrimSpace(line)
565+
if len(line) > 80 {
566+
return line[:77] + "..."
567+
}
568+
return line
569+
}
570+
}
571+
572+
// No package-specific line found; fall back to the general parser but
573+
// indicate the package was not installed after the batch attempt.
574+
parsed := parseBrewError(output)
575+
if parsed == "unknown error" {
576+
return "not installed after batch attempt"
577+
}
578+
return parsed
579+
}
580+
588581
func Uninstall(packages []string, dryRun bool) error {
589582
if len(packages) == 0 {
590583
return nil

internal/brew/brew_test.go

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package brew
22

33
import (
4+
"os"
45
"strings"
56
"testing"
67

78
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
810
)
911

1012
func TestParseBrewError(t *testing.T) {
@@ -198,17 +200,71 @@ func TestHandleFailedJobs_WithFailures(t *testing.T) {
198200
// commands (brew install pkg1 pkg2...) instead of individual commands.
199201
// This leverages Homebrew's native parallel download capability.
200202
func TestInstallWithProgress_BatchMode(t *testing.T) {
201-
// Dry-run should show batch commands
202-
formulae, casks, err := InstallWithProgress(
203+
// Capture stdout to verify batch command format
204+
oldStdout := os.Stdout
205+
r, w, err := os.Pipe()
206+
require.NoError(t, err)
207+
os.Stdout = w
208+
209+
formulae, casks, runErr := InstallWithProgress(
203210
[]string{"git", "curl", "wget"},
204211
[]string{"firefox", "chrome"},
205212
true,
206213
)
207-
assert.NoError(t, err)
208-
assert.Empty(t, formulae)
209-
assert.Empty(t, casks)
210-
// In dry-run mode, we can't easily verify the command format without capturing output,
211-
// but the function signature and behavior tests ensure batch mode is used
214+
215+
w.Close()
216+
os.Stdout = oldStdout
217+
218+
var buf strings.Builder
219+
_, copyErr := buf.ReadFrom(r)
220+
require.NoError(t, copyErr)
221+
output := buf.String()
222+
223+
assert.NoError(t, runErr)
224+
assert.Empty(t, formulae, "dry-run should not report installed formulae")
225+
assert.Empty(t, casks, "dry-run should not report installed casks")
226+
227+
// Verify batch command format: all CLI packages in a single brew install
228+
assert.Contains(t, output, "brew install git curl wget",
229+
"dry-run should show a single batch brew install command for all CLI packages")
230+
// Verify batch cask command format
231+
assert.Contains(t, output, "brew install --cask firefox chrome",
232+
"dry-run should show a single batch brew install --cask command for all cask packages")
233+
}
234+
235+
func TestExtractPackageError(t *testing.T) {
236+
tests := []struct {
237+
name string
238+
output string
239+
pkg string
240+
expected string
241+
}{
242+
{
243+
name: "package-specific error line",
244+
output: "==> Installing foo\nError: foo: no bottle available!\n==> Installing bar",
245+
pkg: "foo",
246+
expected: "Error: foo: no bottle available!",
247+
},
248+
{
249+
name: "no package-specific line falls back to parser",
250+
output: "Error: No internet connection available",
251+
pkg: "baz",
252+
expected: "no internet connection",
253+
},
254+
{
255+
name: "no useful output gives batch message",
256+
output: "some random output\nnothing useful here",
257+
pkg: "qux",
258+
expected: "not installed after batch attempt",
259+
},
260+
}
261+
262+
for _, tt := range tests {
263+
t.Run(tt.name, func(t *testing.T) {
264+
result := extractPackageError(tt.output, tt.pkg)
265+
assert.Equal(t, tt.expected, result)
266+
})
267+
}
212268
}
213269

214270
// TestResolveFormulaName tests that formula aliases are resolved correctly.

0 commit comments

Comments
 (0)