Skip to content

Commit

Permalink
Improve pod install error message (#90)
Browse files Browse the repository at this point in the history
* CocoapodsInstaller uses latest go-utils and go-steputils dependencies

* Implement cocoapods command error finder

* Test CocoapodsInstaller

* Update go-utils and go-xcode

* Improve error message for transient errors

* Update error messages

* Migrate CocoapodsInstaller to go-utils/v2/log

* Rename cocoapodsinstaller to cocoapods_installer

* findErrors if statement optimization

* Test if first pod install fails

* Cleanup pod installer tests
  • Loading branch information
godrei authored Feb 7, 2023
1 parent 1ae8eac commit ad1981e
Show file tree
Hide file tree
Showing 68 changed files with 7,680 additions and 362 deletions.
121 changes: 121 additions & 0 deletions cocoapods_installer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package main

import (
"bufio"
"fmt"
"os"
"strings"

"github.com/bitrise-io/go-steputils/v2/ruby"
"github.com/bitrise-io/go-utils/v2/command"
"github.com/bitrise-io/go-utils/v2/errorutil"
"github.com/bitrise-io/go-utils/v2/log"
)

// CocoapodsInstaller ...
type CocoapodsInstaller struct {
rubyCmdFactory ruby.CommandFactory
logger log.Logger
}

// NewCocoapodsInstaller ...
func NewCocoapodsInstaller(rubyCmdFactory ruby.CommandFactory, logger log.Logger) CocoapodsInstaller {
return CocoapodsInstaller{
rubyCmdFactory: rubyCmdFactory,
logger: logger,
}
}

// InstallPods ...
func (i CocoapodsInstaller) InstallPods(podArg []string, podCmd string, podfileDir string, verbose bool) error {
if err := i.runPodInstall(podArg, podCmd, podfileDir, verbose); err == nil {
return nil
} else {
i.logger.Printf("")
i.logger.Warnf(errorutil.FormattedError(fmt.Errorf("Failed to install Pods: %w", err)))
i.logger.Warnf("Retrying with pod repo update...")
i.logger.Printf("")
}

if err := i.runPodRepoUpdate(podArg, podfileDir, verbose); err != nil {
return err
}

if err := i.runPodInstall(podArg, podCmd, podfileDir, verbose); err != nil {
return err
}

return nil
}

func (i CocoapodsInstaller) runPodInstall(podArg []string, podCmd string, podfileDir string, verbose bool) error {
errorFinder := &cocoapodsCmdErrorFinder{}
cmdSlice := podInstallCmdSlice(podArg, podCmd, verbose)
cmd := createPodCommand(i.rubyCmdFactory, cmdSlice, podfileDir, errorFinder)
i.logger.Donef("$ %s", cmd.PrintableCommandArgs())
return cmd.Run()
}

func (i CocoapodsInstaller) runPodRepoUpdate(podArg []string, podfileDir string, verbose bool) error {
errorFinder := &cocoapodsCmdErrorFinder{}
cmdSlice := podRepoUpdateCmdSlice(podArg, verbose)
cmd := createPodCommand(i.rubyCmdFactory, cmdSlice, podfileDir, errorFinder)
i.logger.Donef("$ %s", cmd.PrintableCommandArgs())
return cmd.Run()
}

func podInstallCmdSlice(podArg []string, podCmd string, verbose bool) []string {
cmdSlice := append(podArg, podCmd, "--no-repo-update")
if verbose {
cmdSlice = append(cmdSlice, "--verbose")
}
return cmdSlice
}

func podRepoUpdateCmdSlice(podArg []string, verbose bool) []string {
cmdSlice := append(podArg, "repo", "update")
if verbose {
cmdSlice = append(cmdSlice, "--verbose")
}
return cmdSlice
}

func createPodCommand(factory ruby.CommandFactory, args []string, dir string, errorFinder *cocoapodsCmdErrorFinder) command.Command {
return factory.Create(args[0], args[1:], &command.Opts{
Stdout: os.Stdout,
Stderr: os.Stderr,
Stdin: nil,
Env: nil,
Dir: dir,
ErrorFinder: errorFinder.findErrors,
})
}

type cocoapodsCmdErrorFinder struct {
transientProblemAlreadySeen bool
}

func (f *cocoapodsCmdErrorFinder) findErrors(out string) []string {
var errors []string

reader := strings.NewReader(out)
scanner := bufio.NewScanner(reader)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
line := scanner.Text()

if strings.HasPrefix(line, "[!] ") || strings.HasPrefix(line, "curl: ") {
errors = append(errors, line)
} else if strings.HasPrefix(line, "Warning: Transient problem: ") {
if !f.transientProblemAlreadySeen {
errors = append(errors, "Transient problem")
f.transientProblemAlreadySeen = true
}
}
}
if err := scanner.Err(); err != nil {
return nil
}

return errors
}
184 changes: 184 additions & 0 deletions cocoapods_installer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package main

import (
"errors"
"strings"
"testing"

"bitrise-steplib/steps-cocoapods-install/mocks"

"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

func Test_GivenCocoapodsInstaller_WhenArgsGiven_ThenRunsExpectedCommand(t *testing.T) {
type args struct {
podArg []string
podCmd string
verbose bool
}
tests := []struct {
name string
args args
wantCmd []string
}{
{
name: "simple pod install",
args: args{podArg: []string{"pod"}, podCmd: "install", verbose: false},
wantCmd: []string{"pod", "install", "--no-repo-update"},
},
{
name: "verbose pod install",
args: args{podArg: []string{"pod"}, podCmd: "install", verbose: true},
wantCmd: []string{"pod", "install", "--no-repo-update", "--verbose"},
},
{
name: "verbose pod update",
args: args{podArg: []string{"pod"}, podCmd: "update", verbose: true},
wantCmd: []string{"pod", "update", "--no-repo-update", "--verbose"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Given
cmd := new(mocks.Command)
cmd.On("PrintableCommandArgs").Return(strings.Join(tt.wantCmd, " "))
cmd.On("Run").Return(nil)

cmdFactory := new(mocks.CommandFactory)
cmdFactory.On("Create", tt.wantCmd[0], tt.wantCmd[1:], mock.Anything).Return(cmd)

logger := new(mocks.Logger)
logger.On("Donef", mock.Anything, mock.Anything)

installer := NewCocoapodsInstaller(cmdFactory, logger)

// When
err := installer.InstallPods(tt.args.podArg, tt.args.podCmd, "", tt.args.verbose)

// Then
require.NoError(t, err)
cmdFactory.AssertExpectations(t)
cmd.AssertExpectations(t)
})
}
}

func Test_GivenCocoapodsInstaller_WhenInstallFails_ThenRunsRepoUpdateAndRetries(t *testing.T) {
// Given
podArg := []string{"pod"}
podCmd := "install"

firstInstallCmd := new(mocks.Command)
firstInstallCmd.On("PrintableCommandArgs").Return(mock.Anything)
firstInstallCmd.On("Run").Return(errors.New("[!] Error installing boost")).Once()

repoUpdateCmd := new(mocks.Command)
repoUpdateCmd.On("PrintableCommandArgs").Return(mock.Anything)
repoUpdateCmd.On("Run").Return(nil).Once()

secondInstallCmd := new(mocks.Command)
secondInstallCmd.On("PrintableCommandArgs").Return(mock.Anything)
secondInstallCmd.On("Run").Return(nil).Once()

cmdFactory := new(mocks.CommandFactory)
cmdFactory.On("Create", podArg[0], []string{podCmd, "--no-repo-update"}, mock.Anything).Return(firstInstallCmd).Once()
cmdFactory.On("Create", podArg[0], []string{"repo", "update"}, mock.Anything).Return(repoUpdateCmd).Once()
cmdFactory.On("Create", podArg[0], []string{podCmd, "--no-repo-update"}, mock.Anything).Return(secondInstallCmd).Once()

logger := new(mocks.Logger)
logger.On("Donef", mock.Anything, mock.Anything)
logger.On("Printf", mock.Anything, mock.Anything)
logger.On("Warnf", mock.Anything, mock.Anything)

installer := NewCocoapodsInstaller(cmdFactory, logger)

// When
err := installer.InstallPods(podArg, podCmd, "", false)

// Then
require.NoError(t, err)
cmdFactory.AssertExpectations(t)
firstInstallCmd.AssertExpectations(t)
repoUpdateCmd.AssertExpectations(t)
secondInstallCmd.AssertExpectations(t)
}

func Test_GivenCocoapodsErrorFinder_WhenGatewayTimeOut_ThenFindsErrors(t *testing.T) {
expectedErrors := []string{
"[!] Error installing boost",
"[!] /usr/bin/curl -f -L -o /var/folders/v9/hjkgcpmn6bq99p7gvyhpq6800000gn/T/d20221018-7204-3bfs7/file.tbz https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.tar.bz2 --create-dirs --netrc-optional --retry 2 -A 'CocoaPods/1.11.3 cocoapods-downloader/1.6.3'",
"Transient problem",
"curl: (22) The requested URL returned error: 504 Gateway Time-out",
}
errorFinder := cocoapodsCmdErrorFinder{}
errors := errorFinder.findErrors(podInstallGatewayTimeOutError)
require.Equal(t, expectedErrors, errors)
}

func Test_GivenCocoapodsErrorFinder_WhenBadGateway_ThenFindsErrors(t *testing.T) {
expectedErrors := []string{
"[!] Error installing boost",
"[!] /usr/bin/curl -f -L -o /var/folders/v9/hjkgcpmn6bq99p7gvyhpq6800000gn/T/d20221018-3650-smj60t/file.tbz https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.tar.bz2 --create-dirs --netrc-optional --retry 2 -A 'CocoaPods/1.11.3 cocoapods-downloader/1.6.3'",
"Transient problem",
"curl: (22) The requested URL returned error: 502 Bad Gateway",
}
errorFinder := cocoapodsCmdErrorFinder{}
errors := errorFinder.findErrors(podInstallBadGatewayError)
require.Equal(t, expectedErrors, errors)
}

const podInstallGatewayTimeOutError = `Installing YogaKit (1.18.1)
Installing boost (1.76.0)
[!] Error installing boost
[!] /usr/bin/curl -f -L -o /var/folders/v9/hjkgcpmn6bq99p7gvyhpq6800000gn/T/d20221018-7204-3bfs7/file.tbz https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.tar.bz2 --create-dirs --netrc-optional --retry 2 -A 'CocoaPods/1.11.3 cocoapods-downloader/1.6.3'
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
0 0 0 0 0 0 0 0 --:--:-- 0:00:01 --:--:-- 0
0 0 0 0 0 0 0 0 --:--:-- 0:00:01 --:--:-- 0
Warning: Transient problem: HTTP error Will retry in 1 seconds. 2 retries
Warning: left.
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
0 0 0 0 0 0 0 0 --:--:-- 0:00:01 --:--:-- 0
0 0 0 0 0 0 0 0 --:--:-- 0:01:15 --:--:-- 0
Warning: Transient problem: HTTP error Will retry in 2 seconds. 1 retries
Warning: left.
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
0 0 0 0 0 0 0 0 --:--:-- 0:00:01 --:--:-- 0
0 0 0 0 0 0 0 0 --:--:-- 0:01:15 --:--:-- 0
curl: (22) The requested URL returned error: 504 Gateway Time-out
`

const podInstallBadGatewayError = `Installing YogaKit (1.18.1)
Installing boost (1.76.0)
[!] Error installing boost
[!] /usr/bin/curl -f -L -o /var/folders/v9/hjkgcpmn6bq99p7gvyhpq6800000gn/T/d20221018-3650-smj60t/file.tbz https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.tar.bz2 --create-dirs --netrc-optional --retry 2 -A 'CocoaPods/1.11.3 cocoapods-downloader/1.6.3'
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
0 0 0 0 0 0 0 0 --:--:-- 0:00:01 --:--:-- 0
0 0 0 0 0 0 0 0 --:--:-- 0:01:05 --:--:-- 0
Warning: Transient problem: HTTP error Will retry in 1 seconds. 2 retries
Warning: left.
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
0 0 0 0 0 0 0 0 --:--:-- 0:00:01 --:--:-- 0
0 0 0 0 0 0 0 0 --:--:-- 0:01:15 --:--:-- 0
Warning: Transient problem: HTTP error Will retry in 2 seconds. 1 retries
Warning: left.
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
0 0 0 0 0 0 0 0 --:--:-- 0:00:01 --:--:-- 0
curl: (22) The requested URL returned error: 502 Bad Gateway`
9 changes: 6 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ go 1.15

require (
github.com/bitrise-io/go-steputils v1.0.4
github.com/bitrise-io/go-utils v1.0.1
github.com/bitrise-io/go-xcode v1.0.3
github.com/stretchr/testify v1.7.0
github.com/bitrise-io/go-steputils/v2 v2.0.0-alpha.16
github.com/bitrise-io/go-utils v1.0.4
github.com/bitrise-io/go-utils/v2 v2.0.0-alpha.15
github.com/bitrise-io/go-xcode v1.0.10
github.com/stretchr/testify v1.8.1
golang.org/x/text v0.6.0 // indirect
)
Loading

0 comments on commit ad1981e

Please sign in to comment.