Skip to content

Commit

Permalink
Improve Gradle command error messages (#99)
Browse files Browse the repository at this point in the history
* Improve gradle errors by utilizing stderr

* Code cleanup

* Introduce formatted errors

* Improve error messages

* Fix lint issues

* Update bitrise.yml

* Update bitrise.yml

* Update errors.go

* Update errors.go

* command v2

* Test scan dependencies error

* Get back to go-utils v2.0.0-alpha.14

* Update androidcomponents_test.go
  • Loading branch information
godrei authored Jul 4, 2023
1 parent b77cfa8 commit 2d89aef
Show file tree
Hide file tree
Showing 90 changed files with 29,919 additions and 149 deletions.
65 changes: 51 additions & 14 deletions androidcomponents/androidcomponents.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package androidcomponents
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
Expand All @@ -19,13 +21,19 @@ import (
"github.com/bitrise-io/go-utils/pathutil"
"github.com/bitrise-io/go-utils/retry"
"github.com/bitrise-io/go-utils/sliceutil"
commandv2 "github.com/bitrise-io/go-utils/v2/command"
"github.com/bitrise-io/go-utils/v2/env"
logv2 "github.com/bitrise-io/go-utils/v2/log"
)

type installer struct {
androidSDK *sdk.Model
sdkManager *sdkmanager.Model
gradlewPath string
gradlewDependenciesOptions []string

factory commandv2.Factory
logger logv2.Logger
}

// InstallLicences ...
Expand Down Expand Up @@ -95,13 +103,17 @@ func Ensure(androidSdk *sdk.Model, gradlewPath string, gradlewDependenciesOption
sdkManager: sdkManager,
gradlewPath: gradlewPath,
gradlewDependenciesOptions: gradlewDependenciesOptions,

factory: commandv2.NewFactory(env.NewRepository()),
logger: logv2.NewLogger(),
}

return retry.Times(1).Wait(time.Second).Try(func(attempt uint) error {
retryNum := uint(1)
return retry.Times(retryNum).Wait(time.Second).Try(func(attempt uint) error {
if attempt > 0 {
log.Warnf("Retrying...")
}
return i.scanDependencies()
return i.scanDependencies(attempt == retryNum)
})
}

Expand All @@ -114,22 +126,37 @@ func (i installer) getDependencyCases() map[string]func(match string) error {
}
}

func getDependenciesOutput(projectLocation string, options []string) (string, error) {
func (i installer) getDependenciesOutput(projectLocation string, options []string) (string, error) {
args := []string{"dependencies", "--stacktrace"}
args = append(args, options...)

gradleCmd := command.New("./gradlew", args...)
gradleCmd.SetStdin(strings.NewReader("y"))
gradleCmd.SetDir(projectLocation)
return gradleCmd.RunAndReturnTrimmedCombinedOutput()
var outBuffer bytes.Buffer
var errBuffer bytes.Buffer

gradleCmd := i.factory.Create("./gradlew", args, &commandv2.Opts{
Stdout: &outBuffer,
Stderr: io.MultiWriter(&errBuffer, &outBuffer),
Stdin: strings.NewReader("y"),
Dir: projectLocation,
})
err := gradleCmd.Run()
out := outBuffer.String()
if err != nil {
return out, NewCommandError(gradleCmd.PrintableCommandArgs(), err, errBuffer.String())
}
return "", nil
}

func (i installer) scanDependencies(foundMatches ...string) error {
out, err := getDependenciesOutput(filepath.Dir(i.gradlewPath), i.gradlewDependenciesOptions)
if err == nil {
func (i installer) scanDependencies(isLastAttempt bool, foundMatches ...string) error {
out, getDependenciesErr := i.getDependenciesOutput(filepath.Dir(i.gradlewPath), i.gradlewDependenciesOptions)
if getDependenciesErr == nil {
return nil
}
err = fmt.Errorf("output: %s\nerror: %s", out, err)
var executionErr CommandExecutionError
if errors.As(getDependenciesErr, &executionErr) {
return getDependenciesErr
}

scanner := bufio.NewScanner(strings.NewReader(out))
for scanner.Scan() {
line := scanner.Text()
Expand All @@ -143,15 +170,25 @@ func (i installer) scanDependencies(foundMatches ...string) error {
log.Printf(out)
return callbackErr
}
return i.scanDependencies(append(foundMatches, matches[1])...)
return i.scanDependencies(isLastAttempt, append(foundMatches, matches[1])...)
}
}
}
if scanner.Err() != nil {
log.Printf(out)
if isLastAttempt {
log.Printf(out)
}
return scanner.Err()
}
return err

if isLastAttempt {
var exitErr CommandExitError
if errors.As(getDependenciesErr, &exitErr) {
i.logger.Printf(out)
}
}

return getDependenciesErr
}

func (i installer) target(version string) error {
Expand Down
90 changes: 90 additions & 0 deletions androidcomponents/androidcomponents_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package androidcomponents

import (
"errors"
"io"
"os/exec"
"testing"

commandv2 "github.com/bitrise-io/go-utils/v2/command"
"github.com/bitrise-io/go-utils/v2/env"
"github.com/bitrise-steplib/steps-install-missing-android-tools/mocks"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

func Test_GivenCommand_WhenFails_ThenReturnsExitError(t *testing.T) {
// TODO: androidcomponents.NewCommandError requires a command execution function to return an *exec.ExitError,
// when the command was successfully executed, but returned non-zero exit status.
// In go-utils/[email protected] the command package was updated to return a new custom error.
// Upgrading to this or higher version breaks androidcomponents.NewCommandError.
// This test ensures that the used go-utils/v2/command package works well with androidcomponents.NewCommandError.
factory := commandv2.NewFactory(env.NewRepository())
cmd := factory.Create("bash", []string{"-c", "exit 1"}, nil)
err := cmd.Run()
var exitErr *exec.ExitError
require.True(t, errors.As(err, &exitErr))
}

func Test_GivenInstallerAndGradlePrintsToStderr_WhenScanDependencies_ThenErrorContainStderr(t *testing.T) {
// Given
var stderr io.Writer

command := new(mocks.Command)
command.On("Run").Run(func(args mock.Arguments) {
_, err := stderr.Write([]byte("error reason"))
require.NoError(t, err)
}).Return(&exec.ExitError{})
command.On("PrintableCommandArgs").Return("./gradlew dependencies --stacktrace")

factory := new(mocks.Factory)
factory.On("Create", "./gradlew", []string{"dependencies", "--stacktrace"}, mock.Anything).Run(func(args mock.Arguments) {
opts := args.Get(2).(*commandv2.Opts)
stderr = opts.Stderr
}).Return(command)

installer := installer{
gradlewPath: "./gradlew",
factory: factory,
}

// When
err := installer.scanDependencies(false)

// Then
require.EqualError(t, err, "command failed with exit status -1 (./gradlew dependencies --stacktrace): error reason")
}

func Test_GivenInstallerAndGradleDoesNotPrintToStderr_WhenScanDependenciesAndLastAttempt_ThenErrorGenericErrorThrownAndStdoutLogged(t *testing.T) {
// Given
var stdout io.Writer

command := new(mocks.Command)
command.On("Run").Run(func(args mock.Arguments) {
_, err := stdout.Write([]byte("Task failed"))
require.NoError(t, err)
}).Return(&exec.ExitError{})
command.On("PrintableCommandArgs").Return("./gradlew dependencies --stacktrace")

factory := new(mocks.Factory)
factory.On("Create", "./gradlew", []string{"dependencies", "--stacktrace"}, mock.Anything).Run(func(args mock.Arguments) {
opts := args.Get(2).(*commandv2.Opts)
stdout = opts.Stdout
}).Return(command)

logger := new(mocks.Logger)
logger.On("Printf", "Task failed").Return()

installer := installer{
gradlewPath: "./gradlew",
factory: factory,
logger: logger,
}

// When
err := installer.scanDependencies(true)

// Then
require.EqualError(t, err, "command failed with exit status -1 (./gradlew dependencies --stacktrace): check the command's output for details")
logger.AssertExpectations(t)
}
84 changes: 84 additions & 0 deletions androidcomponents/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package androidcomponents

import (
"errors"
"fmt"
"os/exec"
)

func NewCommandError(cmd string, err error, reason string) error {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
if len(reason) == 0 {
return NewCommandExitError(cmd, exitErr)
}

return NewCommandExitErrorWithReason(cmd, exitErr, reason)
}

return NewCommandExecutionError(cmd, err)
}

type CommandExecutionError struct {
cmd string
err error
}

func NewCommandExecutionError(cmd string, err error) CommandExecutionError {
return CommandExecutionError{
cmd: cmd,
err: err,
}
}

func (e CommandExecutionError) Error() string {
return fmt.Sprintf("executing command failed (%s): %s", e.cmd, e.err)
}

func (e CommandExecutionError) Unwrap() error {
return e.err
}

type CommandExitError struct {
cmd string
err *exec.ExitError
suggestion error
}

func NewCommandExitError(cmd string, err *exec.ExitError) CommandExitError {
return CommandExitError{
cmd: cmd,
err: err,
suggestion: errors.New("check the command's output for details"),
}
}

func (e CommandExitError) Error() string {
return fmt.Sprintf("command failed with exit status %d (%s): %s", e.err.ExitCode(), e.cmd, e.suggestion)
}

func (e CommandExitError) Unwrap() error {
return e.suggestion
}

type CommandExitErrorWithReason struct {
cmd string
err *exec.ExitError
reason error
}

func NewCommandExitErrorWithReason(cmd string, err *exec.ExitError, reason string) CommandExitErrorWithReason {
return CommandExitErrorWithReason{
cmd: cmd,
err: err,
reason: errors.New(reason),
}
}

func (e CommandExitErrorWithReason) Error() string {
return fmt.Sprintf("command failed with exit status %d (%s): %s", e.err.ExitCode(), e.cmd, e.reason)
}

func (e CommandExitErrorWithReason) Unwrap() error {
return e.reason
}
26 changes: 0 additions & 26 deletions e2e/bitrise.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,6 @@ workflows:
- _run_and_build
- _restore_ndk_bundle

test_react_native:
envs:
- SAMPLE_APP_URL: https://github.com/bitrise-io/Bitrise-React-Native-Sample.git
- BRANCH: master
- GRADLE_BUILD_FILE_PATH: android/build.gradle
- GRADLEW_PATH: android/gradlew
before_run:
- _setup
- _backup_ndk_bundle
after_run:
- _restore_ndk_bundle
steps:
- script:
inputs:
- content: |-
#!/usr/bin/env bash
set -ex
yarn
- path::./:
title: Execute step
inputs:
- ndk_version: 23.1.7779620
- gradle-runner:
inputs:
- gradle_task: assembleDebug

test_ndk_install:
summary: Test installing multiple NDK revisions and compare installed version numbers to expected values
envs:
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ require (
github.com/bitrise-io/go-android v1.0.0
github.com/bitrise-io/go-steputils v1.0.1
github.com/bitrise-io/go-utils v1.0.1
github.com/bitrise-io/go-utils/v2 v2.0.0-alpha.14.0.20221208123037-ab31edd851e5
github.com/hashicorp/go-version v1.3.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/stretchr/testify v1.8.4
)
Loading

0 comments on commit 2d89aef

Please sign in to comment.