Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Gradle command error messages #99

Merged
merged 13 commits into from
Jul 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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