From e4442ab322e9f10ed2fca78ecb3f4fbe7c979ee3 Mon Sep 17 00:00:00 2001 From: Michael Chernov <4ernovm@gmail.com> Date: Tue, 12 Dec 2023 20:49:40 +0200 Subject: [PATCH] Gradle error enhancements (#165) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Enhance Gradle errors * Make wording changes * Fix build errors * fix lint errors and tests --------- Co-authored-by: Emil Wåreus --- internal/resolution/job/error.go | 12 ++ internal/resolution/job/errors.go | 9 ++ internal/resolution/job/errors_test.go | 21 +++ internal/resolution/pm/gradle/job.go | 149 +++++++++++++++++++++- internal/resolution/pm/gradle/job_test.go | 93 +++++++++++--- internal/resolution/pm/util/error.go | 26 ++-- 6 files changed, 280 insertions(+), 30 deletions(-) diff --git a/internal/resolution/job/error.go b/internal/resolution/job/error.go index cf6ee32e..29b178df 100644 --- a/internal/resolution/job/error.go +++ b/internal/resolution/job/error.go @@ -5,9 +5,11 @@ type IError interface { Command() string Documentation() string Status() string + IsCritical() bool SetStatus(string) SetDocumentation(string) SetCommand(string) + SetIsCritical(bool) } type BaseJobError struct { @@ -15,6 +17,7 @@ type BaseJobError struct { command string documentation string status string + isCritical bool } func (e BaseJobError) Error() string { @@ -33,6 +36,10 @@ func (e BaseJobError) Status() string { return e.status } +func (e BaseJobError) IsCritical() bool { + return e.isCritical +} + func (e *BaseJobError) SetStatus(status string) { e.status = status } @@ -45,11 +52,16 @@ func (e *BaseJobError) SetCommand(command string) { e.command = command } +func (e *BaseJobError) SetIsCritical(isCritical bool) { + e.isCritical = isCritical +} + func NewBaseJobError(err string) *BaseJobError { return &BaseJobError{ err: err, command: "", documentation: "", status: "", + isCritical: true, } } diff --git a/internal/resolution/job/errors.go b/internal/resolution/job/errors.go index 401c70ea..8a2b2503 100644 --- a/internal/resolution/job/errors.go +++ b/internal/resolution/job/errors.go @@ -3,6 +3,7 @@ package job type IErrors interface { Warning(err IError) Critical(err IError) + Append(err IError) GetWarningErrors() []IError GetCriticalErrors() []IError GetAll() []IError @@ -31,6 +32,14 @@ func (errors *Errors) Critical(err IError) { errors.criticalErrs = append(errors.criticalErrs, err) } +func (errors *Errors) Append(err IError) { + if err.IsCritical() { + errors.Critical(err) + } else { + errors.Warning(err) + } +} + func (errors *Errors) GetWarningErrors() []IError { return errors.warningErrs } diff --git a/internal/resolution/job/errors_test.go b/internal/resolution/job/errors_test.go index ed7188ea..92429526 100644 --- a/internal/resolution/job/errors_test.go +++ b/internal/resolution/job/errors_test.go @@ -33,6 +33,27 @@ func TestCritical(t *testing.T) { assert.Contains(t, errors.criticalErrs, critical) } +func TestAppend(t *testing.T) { + errors := NewErrors("") + + critical1 := NewBaseJobError("critical") + critical1.SetIsCritical(true) + errors.Append(critical1) + + critical2 := NewBaseJobError("another critical") + errors.Append(critical2) + + warning := NewBaseJobError("warning") + warning.SetIsCritical(false) + errors.Append(warning) + + assert.Len(t, errors.warningErrs, 1) + assert.Len(t, errors.criticalErrs, 2) + assert.Contains(t, errors.criticalErrs, critical1) + assert.Contains(t, errors.criticalErrs, critical2) + assert.Contains(t, errors.warningErrs, warning) +} + func TestGetWarningErrors(t *testing.T) { errors := NewErrors("") warning := NewBaseJobError("error") diff --git a/internal/resolution/pm/gradle/job.go b/internal/resolution/pm/gradle/job.go index 74319921..ae662453 100644 --- a/internal/resolution/pm/gradle/job.go +++ b/internal/resolution/pm/gradle/job.go @@ -3,6 +3,7 @@ package gradle import ( "fmt" "path/filepath" + "regexp" "strings" "github.com/debricked/cli/internal/resolution/job" @@ -10,6 +11,13 @@ import ( "github.com/debricked/cli/internal/resolution/pm/writer" ) +const ( + bugErrRegex = "BUG! (.*)" + notRootDirErrRegex = "Error: (Could not find or load main class .*)" + unrelatedBuildErrRegex = "(Project directory '.*' is not part of the build defined by settings file '.*')" + unknownPropertyErrRegex = "(Could not get unknown property .*)" +) + type Job struct { job.BaseJob dir string @@ -53,22 +61,29 @@ func (j *Job) Run() { if err != nil { if permissionErr != nil { - j.Errors().Critical(util.NewPMJobError(permissionErr.Error())) + j.handleError(util.NewPMJobError(permissionErr.Error())) } - j.Errors().Critical(util.NewPMJobError(err.Error())) + cmdErr := util.NewPMJobError(err.Error()) + cmdErr.SetCommand(strings.Trim(dependenciesCmd.String(), " ")) + j.handleError(cmdErr) return } - j.SendStatus("creating dependency graph") + status := "creating dependency graph" + j.SendStatus(status) _, err = dependenciesCmd.Output() if permissionErr != nil { - j.Errors().Warning(util.NewPMJobError(permissionErr.Error())) + cmdErr := util.NewPMJobError(permissionErr.Error()) + cmdErr.SetIsCritical(false) + j.handleError(cmdErr) } if err != nil { - j.Errors().Critical(util.NewPMJobError(j.GetExitError(err).Error())) + cmdErr := util.NewPMJobError(j.GetExitError(err).Error()) + cmdErr.SetCommand(dependenciesCmd.String()) + j.handleError(cmdErr) return } @@ -77,3 +92,127 @@ func (j *Job) Run() { func (j *Job) GetDir() string { return j.dir } + +func (j *Job) handleError(cmdErr job.IError) { + expressions := []string{ + bugErrRegex, + notRootDirErrRegex, + unrelatedBuildErrRegex, + unknownPropertyErrRegex, + } + + for _, expression := range expressions { + regex := regexp.MustCompile(expression) + + if regex.MatchString(cmdErr.Error()) { + cmdErr = j.addDocumentation(expression, regex, cmdErr) + j.Errors().Append(cmdErr) + + return + } + } + + j.Errors().Append(cmdErr) +} + +func (j *Job) addDocumentation(expr string, regex *regexp.Regexp, cmdErr job.IError) job.IError { + switch { + case expr == bugErrRegex: + cmdErr = j.addBugErrorDocumentation(regex, cmdErr) + case expr == notRootDirErrRegex: + cmdErr = j.addNotRootDirErrorDocumentation(regex, cmdErr) + case expr == unrelatedBuildErrRegex: + cmdErr = j.addUnrelatedBuildErrorDocumentation(regex, cmdErr) + case expr == unknownPropertyErrRegex: + cmdErr = j.addUnknownPropertyErrorDocumentation(regex, cmdErr) + } + + return cmdErr +} + +func (j *Job) addBugErrorDocumentation(regex *regexp.Regexp, cmdErr job.IError) job.IError { + matches := regex.FindAllStringSubmatch(cmdErr.Error(), 1) + message := "" + if len(matches) > 0 && len(matches[0]) > 1 { + message = matches[0][1] + } + + cmdErr.SetDocumentation( + strings.Join( + []string{ + "Failed to build Gradle dependency tree. ", + "The process has failed with following error: ", + message, + ". ", + "Try running the command below with --stacktrace flag to get a stacktrace. ", + "Replace --stacktrace with --info or --debug option to get more log output. ", + "Or with --scan to get full insights.", + }, ""), + ) + + return cmdErr +} + +func (j *Job) addNotRootDirErrorDocumentation(regex *regexp.Regexp, cmdErr job.IError) job.IError { + matches := regex.FindAllStringSubmatch(cmdErr.Error(), 1) + message := "" + if len(matches) > 0 && len(matches[0]) > 1 { + message = matches[0][1] + } + + cmdErr.SetDocumentation( + strings.Join( + []string{ + "Failed to build Gradle dependency tree.", + "The process has failed with following error: " + message + ".", //nolint:all + "You are probably not running the command from the root directory.", + }, " "), + ) + + return cmdErr +} + +func (j *Job) addUnrelatedBuildErrorDocumentation(regex *regexp.Regexp, cmdErr job.IError) job.IError { + matches := regex.FindAllStringSubmatch(cmdErr.Error(), 1) + message := "" + if len(matches) > 0 && len(matches[0]) > 1 { + message = matches[0][1] + } + + cmdErr.SetDocumentation( + strings.Join( + []string{ + "Failed to build Gradle dependency tree. ", + "The process has failed with following error: ", + message, + ". ", + "This error might be caused by inclusion of test folders into resolve process. ", + "Try running resolve command with -e flag. ", + "For example, `debricked resolve -e \"**/test*/**\"` will exclude all folders that start from 'test' from resolution process. ", + "Or if this is an unrelated build, it must have its own settings file.", + }, ""), + ) + + return cmdErr +} + +func (j *Job) addUnknownPropertyErrorDocumentation(regex *regexp.Regexp, cmdErr job.IError) job.IError { + matches := regex.FindAllStringSubmatch(cmdErr.Error(), 1) + message := "" + if len(matches) > 0 && len(matches[0]) > 1 { + message = matches[0][1] + } + + cmdErr.SetDocumentation( + strings.Join( + []string{ + "Failed to build Gradle dependency tree. ", + "The process has failed with following error: ", + message, + ". ", + "Please check your settings.gradle file for errors.", + }, ""), + ) + + return cmdErr +} diff --git a/internal/resolution/pm/gradle/job_test.go b/internal/resolution/pm/gradle/job_test.go index 154aeddb..ae82d926 100644 --- a/internal/resolution/pm/gradle/job_test.go +++ b/internal/resolution/pm/gradle/job_test.go @@ -20,15 +20,53 @@ func TestNewJob(t *testing.T) { } func TestRunCmdErr(t *testing.T) { - cmdErr := errors.New("cmd-error") - j := NewJob("file", "dir", "nil", "nil", testdata.CmdFactoryMock{Err: cmdErr}, writer.FileWriter{}) - - go jobTestdata.WaitStatus(j) - - j.Run() - - assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), util.NewPMJobError(cmdErr.Error())) + cases := []struct { + cmd string + error string + doc string + }{ + { + cmd: "MakeDependenciesCmd", + error: "cmd-error", + doc: util.UnknownError, + }, + { + cmd: "MakeDependenciesCmd", + error: "* What went wrong:\nCould not open init remapped class cache for 60sdrkd1iuvns7c8vzs3hv858 (/home/asus/.gradle/caches/5.4/scripts-remapped/_gradle_init_script_debricked_9ll3l6asw7d59x4iljlnzgcpd/60sdrkd1iuvns7c8vzs3hv858/inita22655f7e805aaeb10a177dc56aa75ac).\n> Could not open init generic class cache for initialization script '/home/asus/Projects/playground/gradle-retrolambda/.gradle-init-script.debricked.groovy' (/home/asus/.gradle/caches/5.4/scripts/60sdrkd1iuvns7c8vzs3hv858/init/inita22655f7e805aaeb10a177dc56aa75ac).\n > BUG! exception in phase 'semantic analysis' in source unit '_BuildScript_' Unsupported class file major version 57\n", + doc: "Failed to build Gradle dependency tree. The process has failed with following error: exception in phase 'semantic analysis' in source unit '_BuildScript_' Unsupported class file major version 57. Try running the command below with --stacktrace flag to get a stacktrace. Replace --stacktrace with --info or --debug option to get more log output. Or with --scan to get full insights.", + }, + { + cmd: "MakeDependenciesCmd", + error: " |Error: Could not find or load main class org.gradle.wrapper.GradleWrapperMain\n |Caused by: java.lang.ClassNotFoundException: org.gradle.wrapper.GradleWrapperMain\n", + doc: "Failed to build Gradle dependency tree. The process has failed with following error: Could not find or load main class org.gradle.wrapper.GradleWrapperMain. You are probably not running the command from the root directory.", + }, + { + cmd: "MakeDependenciesCmd", + error: " |* What went wrong:\n |Project directory '/home/asus/Projects/playground/protobuf-gradle-plugin/testProjectLite' is not part of the build defined by settings file '/home/asus/Projects/playground/protobuf-gradle-plugin/settings.gradle'. If this is an unrelated build, it must have its own settings file.", + doc: "Failed to build Gradle dependency tree. The process has failed with following error: Project directory '/home/asus/Projects/playground/protobuf-gradle-plugin/testProjectLite' is not part of the build defined by settings file '/home/asus/Projects/playground/protobuf-gradle-plugin/settings.gradle'. This error might be caused by inclusion of test folders into resolve process. Try running resolve command with -e flag. For example, `debricked resolve -e \"**/test*/**\"` will exclude all folders that start from 'test' from resolution process. Or if this is an unrelated build, it must have its own settings file.", + }, + { + cmd: "MakeDependenciesCmd", + error: " |A problem occurred evaluating settings 'protobuf-gradle-plugin'.\n |> Could not get unknown property 'glkjhe' for settings 'protobuf-gradle-plugin' of type org.gradle.initialization.DefaultSettings.", + doc: "Failed to build Gradle dependency tree. The process has failed with following error: Could not get unknown property 'glkjhe' for settings 'protobuf-gradle-plugin' of type org.gradle.initialization.DefaultSettings.. Please check your settings.gradle file for errors.", + }, + } + + for _, c := range cases { + expectedError := util.NewPMJobError(c.error) + expectedError.SetDocumentation(c.doc) + expectedError.SetCommand(c.cmd) + + cmdErr := errors.New(c.error) + j := NewJob("file", "dir", "nil", "nil", testdata.CmdFactoryMock{Err: cmdErr}, writer.FileWriter{}) + + go jobTestdata.WaitStatus(j) + + j.Run() + + assert.Len(t, j.Errors().GetAll(), 1) + assert.Contains(t, j.Errors().GetAll(), expectedError) + } } func TestRunCmdOutputErr(t *testing.T) { @@ -45,41 +83,62 @@ func TestRunCmdOutputErr(t *testing.T) { func TestRunCreateErr(t *testing.T) { createErr := errors.New("create-error") + fileWriterMock := &writerTestdata.FileWriterMock{CreateErr: createErr} - j := NewJob("file", "dir", "gradlew", "path", testdata.CmdFactoryMock{Name: "echo", Err: createErr}, fileWriterMock) + cmdFactoryMock := testdata.CmdFactoryMock{Name: "echo", Err: createErr} + cmd, _ := cmdFactoryMock.MakeDependenciesCmd("") + + expectedError := util.NewPMJobError(createErr.Error()) + expectedError.SetCommand(cmd.String()) + + j := NewJob("file", "dir", "gradlew", "path", cmdFactoryMock, fileWriterMock) go jobTestdata.WaitStatus(j) j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), util.NewPMJobError(createErr.Error())) + assert.Contains(t, j.Errors().GetAll(), expectedError) } func TestRunWriteErr(t *testing.T) { writeErr := errors.New("write-error") - fileWriterMock := &writerTestdata.FileWriterMock{WriteErr: writeErr} - j := NewJob("file", "dir", "", "", testdata.CmdFactoryMock{Name: "echo", Err: writeErr}, fileWriterMock) + + fileWriterMock := &writerTestdata.FileWriterMock{CreateErr: writeErr} + cmdFactoryMock := testdata.CmdFactoryMock{Name: "echo", Err: writeErr} + cmd, _ := cmdFactoryMock.MakeDependenciesCmd("") + + expectedError := util.NewPMJobError(writeErr.Error()) + expectedError.SetCommand(cmd.String()) + + j := NewJob("file", "dir", "", "", cmdFactoryMock, fileWriterMock) go jobTestdata.WaitStatus(j) j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), util.NewPMJobError(writeErr.Error())) + assert.Contains(t, j.Errors().GetAll(), expectedError) } func TestRunCloseErr(t *testing.T) { closeErr := errors.New("close-error") - fileWriterMock := &writerTestdata.FileWriterMock{CloseErr: closeErr} - j := NewJob("file", "dir", "gradlew", "path", testdata.CmdFactoryMock{Name: "echo", Err: closeErr}, fileWriterMock) + + fileWriterMock := &writerTestdata.FileWriterMock{CreateErr: closeErr} + cmdFactoryMock := testdata.CmdFactoryMock{Name: "echo", Err: closeErr} + cmd, _ := cmdFactoryMock.MakeDependenciesCmd("") + + expectedError := util.NewPMJobError(closeErr.Error()) + expectedError.SetCommand(cmd.String()) + + j := NewJob("file", "dir", "gradlew", "path", cmdFactoryMock, fileWriterMock) go jobTestdata.WaitStatus(j) j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), util.NewPMJobError(closeErr.Error())) + assert.Contains(t, j.Errors().GetAll(), expectedError) } func TestRunPermissionFailBeforeOutputErr(t *testing.T) { diff --git a/internal/resolution/pm/util/error.go b/internal/resolution/pm/util/error.go index cccd8657..8d873983 100644 --- a/internal/resolution/pm/util/error.go +++ b/internal/resolution/pm/util/error.go @@ -1,10 +1,11 @@ package util type PMJobError struct { - err string - cmd string - doc string - status string + err string + cmd string + doc string + status string + isCritical bool } var InstallPrivateDependencyMessage = "If this is a private dependency, please make sure that the debricked CLI has access to install it or pre-install it before running the debricked CLI." @@ -30,6 +31,10 @@ func (e PMJobError) Status() string { return e.status } +func (e PMJobError) IsCritical() bool { + return e.isCritical +} + func (e *PMJobError) SetStatus(status string) { e.status = status } @@ -42,11 +47,16 @@ func (e *PMJobError) SetCommand(cmd string) { e.cmd = cmd } +func (e *PMJobError) SetIsCritical(isCritical bool) { + e.isCritical = isCritical +} + func NewPMJobError(err string) *PMJobError { return &PMJobError{ - err: err, - cmd: "", - doc: UnknownError, - status: "", + err: err, + cmd: "", + doc: UnknownError, + status: "", + isCritical: true, } }