diff --git a/internal/resolution/job/base_job_test.go b/internal/resolution/job/base_job_test.go index 3512a749..bfff6895 100644 --- a/internal/resolution/job/base_job_test.go +++ b/internal/resolution/job/base_job_test.go @@ -39,10 +39,11 @@ func TestErrors(t *testing.T) { j := BaseJob{} j.file = testFile j.errs = NewErrors(j.file) - j.errs.Critical(jobErr) + jobError := NewBaseJobError(jobErr.Error()) + j.errs.Critical(jobError) assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), jobErr) + assert.Contains(t, j.Errors().GetAll(), jobError) } func TestSendStatus(t *testing.T) { diff --git a/internal/resolution/job/error.go b/internal/resolution/job/error.go index 029daa29..cf6ee32e 100644 --- a/internal/resolution/job/error.go +++ b/internal/resolution/job/error.go @@ -1,5 +1,55 @@ package job type IError interface { - error + Error() string + Command() string + Documentation() string + Status() string + SetStatus(string) + SetDocumentation(string) + SetCommand(string) +} + +type BaseJobError struct { + err string + command string + documentation string + status string +} + +func (e BaseJobError) Error() string { + return e.err +} + +func (e BaseJobError) Command() string { + return e.command +} + +func (e BaseJobError) Documentation() string { + return e.documentation + "\n" +} + +func (e BaseJobError) Status() string { + return e.status +} + +func (e *BaseJobError) SetStatus(status string) { + e.status = status +} + +func (e *BaseJobError) SetDocumentation(doc string) { + e.documentation = doc +} + +func (e *BaseJobError) SetCommand(command string) { + e.command = command +} + +func NewBaseJobError(err string) *BaseJobError { + return &BaseJobError{ + err: err, + command: "", + documentation: "", + status: "", + } } diff --git a/internal/resolution/job/error_test.go b/internal/resolution/job/error_test.go new file mode 100644 index 00000000..b1b5c15e --- /dev/null +++ b/internal/resolution/job/error_test.go @@ -0,0 +1,75 @@ +package job + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewBaseJobError(t *testing.T) { + error_message := "error" + jobError := NewBaseJobError(error_message) + assert.Equal(t, error_message, jobError.err) + assert.Equal(t, string(""), jobError.documentation) + assert.NotNil(t, jobError) +} + +func TestBaseJobErrorError(t *testing.T) { + jobError := BaseJobError{ + err: "error", + command: "", + documentation: "", + status: "", + } + assert.Equal(t, "error", jobError.Error()) +} + +func TestBaseJobErrorCommand(t *testing.T) { + jobError := BaseJobError{ + err: "", + command: "command", + documentation: "", + status: "", + } + assert.Equal(t, "command", jobError.Command()) +} + +func TestBaseJobErrorSetCommand(t *testing.T) { + jobError := NewBaseJobError("") + assert.Equal(t, "", jobError.Command()) + jobError.SetCommand("command") + assert.Equal(t, "command", jobError.Command()) +} + +func TestBaseJobErrorDocumentation(t *testing.T) { + jobError := BaseJobError{ + err: "", + command: "", + documentation: "documentation", + status: "", + } + assert.Equal(t, "documentation\n", jobError.Documentation()) +} + +func TestBaseJobErrorSetDocumentation(t *testing.T) { + jobError := NewBaseJobError("") + assert.Equal(t, "\n", jobError.Documentation()) + jobError.SetDocumentation("documentation") + assert.Equal(t, "documentation\n", jobError.Documentation()) +} +func TestBaseJobErrorStatus(t *testing.T) { + jobError := BaseJobError{ + err: "", + command: "", + documentation: "", + status: "status", + } + assert.Equal(t, "status", jobError.Status()) +} + +func TestBaseJobErrorSetStatus(t *testing.T) { + jobError := NewBaseJobError("") + assert.Equal(t, "", jobError.Status()) + jobError.SetStatus("status") + assert.Equal(t, "status", jobError.Status()) +} diff --git a/internal/resolution/job/errors_test.go b/internal/resolution/job/errors_test.go index be3dc9a1..ed7188ea 100644 --- a/internal/resolution/job/errors_test.go +++ b/internal/resolution/job/errors_test.go @@ -1,7 +1,6 @@ package job import ( - "fmt" "testing" "github.com/stretchr/testify/assert" @@ -18,7 +17,7 @@ func TestNewErrors(t *testing.T) { func TestWarning(t *testing.T) { errors := NewErrors("") - warning := fmt.Errorf("error") + warning := NewBaseJobError("error") errors.Warning(warning) assert.Empty(t, errors.criticalErrs) assert.Len(t, errors.warningErrs, 1) @@ -27,7 +26,7 @@ func TestWarning(t *testing.T) { func TestCritical(t *testing.T) { errors := NewErrors("") - critical := fmt.Errorf("error") + critical := NewBaseJobError("error") errors.Critical(critical) assert.Empty(t, errors.warningErrs) assert.Len(t, errors.criticalErrs, 1) @@ -36,7 +35,7 @@ func TestCritical(t *testing.T) { func TestGetWarningErrors(t *testing.T) { errors := NewErrors("") - warning := fmt.Errorf("error") + warning := NewBaseJobError("error") errors.Warning(warning) assert.Empty(t, errors.GetCriticalErrors()) assert.Len(t, errors.GetWarningErrors(), 1) @@ -45,7 +44,7 @@ func TestGetWarningErrors(t *testing.T) { func TestGetCriticalErrors(t *testing.T) { errors := NewErrors("") - critical := fmt.Errorf("error") + critical := NewBaseJobError("critical") errors.Critical(critical) assert.Empty(t, errors.GetWarningErrors()) assert.Len(t, errors.GetCriticalErrors(), 1) @@ -54,8 +53,8 @@ func TestGetCriticalErrors(t *testing.T) { func TestGetAll(t *testing.T) { errors := NewErrors("") - warning := fmt.Errorf("warning") - critical := fmt.Errorf("critical") + warning := NewBaseJobError("warning") + critical := NewBaseJobError("critical") errors.Warning(warning) errors.Critical(critical) assert.Len(t, errors.GetAll(), 2) @@ -67,11 +66,10 @@ func TestHasError(t *testing.T) { errors := NewErrors("") assert.False(t, errors.HasError()) - warning := fmt.Errorf("warning") + warning := NewBaseJobError("warning") errors.Warning(warning) assert.True(t, errors.HasError()) - - critical := fmt.Errorf("critical") + critical := NewBaseJobError("critical") errors.Warning(critical) assert.True(t, errors.HasError()) } diff --git a/internal/resolution/pm/composer/job.go b/internal/resolution/pm/composer/job.go index d61211b5..3fd5f087 100644 --- a/internal/resolution/pm/composer/job.go +++ b/internal/resolution/pm/composer/job.go @@ -2,6 +2,7 @@ package composer import ( "github.com/debricked/cli/internal/resolution/job" + "github.com/debricked/cli/internal/resolution/pm/util" ) const ( @@ -37,7 +38,8 @@ func (j *Job) Run() { j.SendStatus("installing dependencies") _, err := j.runInstallCmd() if err != nil { - j.Errors().Critical(err) + cmdErr := util.NewPMJobError(err.Error()) + j.Errors().Critical(cmdErr) return } diff --git a/internal/resolution/pm/composer/job_test.go b/internal/resolution/pm/composer/job_test.go index b9a5ee51..ee6ae4fa 100644 --- a/internal/resolution/pm/composer/job_test.go +++ b/internal/resolution/pm/composer/job_test.go @@ -49,7 +49,6 @@ func TestRunInstallCmdErr(t *testing.T) { j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), cmdErr) } func TestRunInstallCmdOutputErr(t *testing.T) { diff --git a/internal/resolution/pm/gomod/job.go b/internal/resolution/pm/gomod/job.go index 0028f5e4..67e7245f 100644 --- a/internal/resolution/pm/gomod/job.go +++ b/internal/resolution/pm/gomod/job.go @@ -37,7 +37,7 @@ func (j *Job) Run() { graphCmdOutput, err := j.runGraphCmd(workingDirectory) if err != nil { - j.Errors().Critical(err) + j.Errors().Critical(util.NewPMJobError(err.Error())) return } @@ -45,7 +45,7 @@ func (j *Job) Run() { j.SendStatus("creating dependency version list") listCmdOutput, err := j.runListCmd(workingDirectory) if err != nil { - j.Errors().Critical(err) + j.Errors().Critical(util.NewPMJobError(err.Error())) return } @@ -53,7 +53,7 @@ func (j *Job) Run() { j.SendStatus("creating lock file") lockFile, err := j.fileWriter.Create(util.MakePathFromManifestFile(j.GetFile(), fileName)) if err != nil { - j.Errors().Critical(err) + j.Errors().Critical(util.NewPMJobError(err.Error())) return } @@ -66,7 +66,7 @@ func (j *Job) Run() { err = j.fileWriter.Write(lockFile, fileContents) if err != nil { - j.Errors().Critical(err) + j.Errors().Critical(util.NewPMJobError(err.Error())) } } diff --git a/internal/resolution/pm/gomod/job_test.go b/internal/resolution/pm/gomod/job_test.go index 5fb2c4b4..b043cf0a 100644 --- a/internal/resolution/pm/gomod/job_test.go +++ b/internal/resolution/pm/gomod/job_test.go @@ -6,6 +6,7 @@ import ( jobTestdata "github.com/debricked/cli/internal/resolution/job/testdata" "github.com/debricked/cli/internal/resolution/pm/gomod/testdata" + "github.com/debricked/cli/internal/resolution/pm/util" "github.com/debricked/cli/internal/resolution/pm/writer" writerTestdata "github.com/debricked/cli/internal/resolution/pm/writer/testdata" "github.com/stretchr/testify/assert" @@ -27,7 +28,7 @@ func TestRunGraphCmdErr(t *testing.T) { j.Run() - assert.Contains(t, j.Errors().GetCriticalErrors(), cmdErr) + assert.Contains(t, j.Errors().GetCriticalErrors(), util.NewPMJobError(cmdErr.Error())) } func TestRunCmdOutputErr(t *testing.T) { @@ -53,7 +54,7 @@ func TestRunListCmdErr(t *testing.T) { j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), cmdErr) + assert.Contains(t, j.Errors().GetAll(), util.NewPMJobError(cmdErr.Error())) } func TestRunListCmdOutputErr(t *testing.T) { @@ -79,7 +80,7 @@ func TestRunCreateErr(t *testing.T) { j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), createErr) + assert.Contains(t, j.Errors().GetAll(), util.NewPMJobError(createErr.Error())) } func TestRunWriteErr(t *testing.T) { @@ -93,7 +94,7 @@ func TestRunWriteErr(t *testing.T) { j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), writeErr) + assert.Contains(t, j.Errors().GetAll(), util.NewPMJobError(writeErr.Error())) } func TestRunCloseErr(t *testing.T) { @@ -107,7 +108,7 @@ func TestRunCloseErr(t *testing.T) { j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), closeErr) + assert.Contains(t, j.Errors().GetAll(), util.NewPMJobError(closeErr.Error())) } func TestRun(t *testing.T) { diff --git a/internal/resolution/pm/gradle/job.go b/internal/resolution/pm/gradle/job.go index e1637244..74319921 100644 --- a/internal/resolution/pm/gradle/job.go +++ b/internal/resolution/pm/gradle/job.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/debricked/cli/internal/resolution/job" + "github.com/debricked/cli/internal/resolution/pm/util" "github.com/debricked/cli/internal/resolution/pm/writer" ) @@ -52,9 +53,9 @@ func (j *Job) Run() { if err != nil { if permissionErr != nil { - j.Errors().Critical(permissionErr) + j.Errors().Critical(util.NewPMJobError(permissionErr.Error())) } - j.Errors().Critical(err) + j.Errors().Critical(util.NewPMJobError(err.Error())) return } @@ -63,11 +64,11 @@ func (j *Job) Run() { _, err = dependenciesCmd.Output() if permissionErr != nil { - j.Errors().Warning(permissionErr) + j.Errors().Warning(util.NewPMJobError(permissionErr.Error())) } if err != nil { - j.Errors().Critical(j.GetExitError(err)) + j.Errors().Critical(util.NewPMJobError(j.GetExitError(err).Error())) return } diff --git a/internal/resolution/pm/gradle/job_test.go b/internal/resolution/pm/gradle/job_test.go index c04186e7..154aeddb 100644 --- a/internal/resolution/pm/gradle/job_test.go +++ b/internal/resolution/pm/gradle/job_test.go @@ -6,6 +6,7 @@ import ( jobTestdata "github.com/debricked/cli/internal/resolution/job/testdata" "github.com/debricked/cli/internal/resolution/pm/gradle/testdata" + "github.com/debricked/cli/internal/resolution/pm/util" "github.com/debricked/cli/internal/resolution/pm/writer" writerTestdata "github.com/debricked/cli/internal/resolution/pm/writer/testdata" "github.com/stretchr/testify/assert" @@ -27,7 +28,7 @@ func TestRunCmdErr(t *testing.T) { j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), cmdErr) + assert.Contains(t, j.Errors().GetAll(), util.NewPMJobError(cmdErr.Error())) } func TestRunCmdOutputErr(t *testing.T) { @@ -52,7 +53,7 @@ func TestRunCreateErr(t *testing.T) { j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), createErr) + assert.Contains(t, j.Errors().GetAll(), util.NewPMJobError(createErr.Error())) } func TestRunWriteErr(t *testing.T) { @@ -65,7 +66,7 @@ func TestRunWriteErr(t *testing.T) { j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), writeErr) + assert.Contains(t, j.Errors().GetAll(), util.NewPMJobError(writeErr.Error())) } func TestRunCloseErr(t *testing.T) { @@ -78,7 +79,7 @@ func TestRunCloseErr(t *testing.T) { j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), closeErr) + assert.Contains(t, j.Errors().GetAll(), util.NewPMJobError(closeErr.Error())) } func TestRunPermissionFailBeforeOutputErr(t *testing.T) { diff --git a/internal/resolution/pm/maven/job.go b/internal/resolution/pm/maven/job.go index 84f5041d..74d0a636 100644 --- a/internal/resolution/pm/maven/job.go +++ b/internal/resolution/pm/maven/job.go @@ -1,10 +1,10 @@ package maven import ( - "errors" "path/filepath" "github.com/debricked/cli/internal/resolution/job" + "github.com/debricked/cli/internal/resolution/pm/util" ) const ( @@ -27,7 +27,7 @@ func (j *Job) Run() { workingDirectory := filepath.Dir(filepath.Clean(j.GetFile())) cmd, err := j.cmdFactory.MakeDependencyTreeCmd(workingDirectory) if err != nil { - j.Errors().Critical(err) + j.Errors().Critical(util.NewPMJobError(err.Error())) return } @@ -36,9 +36,9 @@ func (j *Job) Run() { output, err = cmd.Output() if err != nil { if output == nil { - j.Errors().Critical(err) + j.Errors().Critical(util.NewPMJobError(err.Error())) } else { - j.Errors().Critical(errors.New(string(output))) + j.Errors().Critical(util.NewPMJobError(string(output))) } } } diff --git a/internal/resolution/pm/maven/job_test.go b/internal/resolution/pm/maven/job_test.go index 78da6a71..1d5f7db1 100644 --- a/internal/resolution/pm/maven/job_test.go +++ b/internal/resolution/pm/maven/job_test.go @@ -6,6 +6,7 @@ import ( jobTestdata "github.com/debricked/cli/internal/resolution/job/testdata" "github.com/debricked/cli/internal/resolution/pm/maven/testdata" + "github.com/debricked/cli/internal/resolution/pm/util" "github.com/stretchr/testify/assert" ) @@ -24,7 +25,7 @@ func TestRunCmdErr(t *testing.T) { j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), cmdErr) + assert.Contains(t, j.Errors().GetAll(), util.NewPMJobError(cmdErr.Error())) } func TestRunCmdOutputErr(t *testing.T) { @@ -50,7 +51,7 @@ func TestRunCmdOutputErrNoOutput(t *testing.T) { // assert empty because, when Output is executed it will allocate memory for the byte slice to contain the standard output. // However since no bytes are sent to standard output err will be empty here. - assert.Empty(t, err) + assert.Empty(t, err.Error()) } func TestRun(t *testing.T) { diff --git a/internal/resolution/pm/nuget/job.go b/internal/resolution/pm/nuget/job.go index eb21254b..b9b4b973 100644 --- a/internal/resolution/pm/nuget/job.go +++ b/internal/resolution/pm/nuget/job.go @@ -5,6 +5,7 @@ import ( "os" "github.com/debricked/cli/internal/resolution/job" + "github.com/debricked/cli/internal/resolution/pm/util" ) const ( @@ -41,7 +42,8 @@ func (j *Job) Run() { output, err := j.runInstallCmd() defer j.cleanupTempCsproj() if err != nil { - j.Errors().Critical(fmt.Errorf("%s\n%s", output, err)) + formatted_error := fmt.Errorf("%s\n%s", output, err) + j.Errors().Critical(util.NewPMJobError(formatted_error.Error())) return } @@ -73,8 +75,9 @@ func (j *Job) cleanupTempCsproj() { if tempFile != "" { // remove the packages.config.csproj file err := osRemoveAll(tempFile) + formatted_error := fmt.Errorf("failed to remove temporary .csproj file: %s", err) if err != nil { - j.Errors().Critical(fmt.Errorf("failed to remove temporary .csproj file: %s", err)) + j.Errors().Critical(util.NewPMJobError(formatted_error.Error())) } } } diff --git a/internal/resolution/pm/nuget/job_test.go b/internal/resolution/pm/nuget/job_test.go index d2926b40..c9e96fb5 100644 --- a/internal/resolution/pm/nuget/job_test.go +++ b/internal/resolution/pm/nuget/job_test.go @@ -6,6 +6,7 @@ import ( jobTestdata "github.com/debricked/cli/internal/resolution/job/testdata" "github.com/debricked/cli/internal/resolution/pm/nuget/testdata" + "github.com/debricked/cli/internal/resolution/pm/util" "github.com/stretchr/testify/assert" ) @@ -62,7 +63,7 @@ func TestRunInstallPackagesConfigRemoveAllErr(t *testing.T) { go jobTestdata.WaitStatus(j) j.Run() errors := j.Errors().GetAll() - assert.Equal(t, errors[0], cmdErrGt) + assert.Equal(t, errors[0], util.NewPMJobError(cmdErrGt.Error())) } @@ -84,7 +85,7 @@ func TestRunInstallCmdErr(t *testing.T) { go jobTestdata.WaitStatus(j) j.Run() - assert.Equal(t, j.Errors().GetAll()[0], cmdErrGt) + assert.Equal(t, j.Errors().GetAll()[0], util.NewPMJobError(cmdErrGt.Error())) } func TestRunInstallCmdOutputErr(t *testing.T) { diff --git a/internal/resolution/pm/pip/job.go b/internal/resolution/pm/pip/job.go index cbf469be..ffde27d5 100644 --- a/internal/resolution/pm/pip/job.go +++ b/internal/resolution/pm/pip/job.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "runtime" "strings" @@ -65,25 +66,32 @@ func (j *Job) Run() { if j.venvPath == "" { return } - j.SendStatus("removing venv") + status := "removing venv" + j.SendStatus(status) err := j.pipCleaner.RemoveAll(j.venvPath) if err != nil { - j.Errors().Critical(err) + cmdErr := util.NewPMJobError(err.Error()) + cmdErr.SetDocumentation("Error when trying to remove previous virtual environment") + cmdErr.SetStatus(status) + j.Errors().Critical(cmdErr) } }() - - j.SendStatus("creating venv") - _, err := j.runCreateVenvCmd() - if err != nil { - j.Errors().Critical(err) + status := "creating venv" + j.SendStatus(status) + _, cmdErr := j.runCreateVenvCmd() + if cmdErr != nil { + cmdErr.SetDocumentation("Error when trying to create python virtual environment") + cmdErr.SetStatus(status) + j.Errors().Critical(cmdErr) return } - - j.SendStatus("installing requirements") - _, err = j.runInstallCmd() - if err != nil { - j.Errors().Critical(err) + status = "installing dependencies" + j.SendStatus(status) + _, cmdErr = j.runInstallCmd() + if cmdErr != nil { + cmdErr.SetStatus(status) + j.handleInstallError(cmdErr) return } @@ -98,28 +106,98 @@ func (j *Job) Run() { } -func (j *Job) writeLockContent() error { - j.SendStatus("generating lock file") - catCmdOutput, err := j.runCatCmd() - if err != nil { - return err +func (j *Job) handleInstallError(cmdErr job.IError) { + var buildError = regexp.MustCompile("setup.py[ install for]*(?P[^ ]*) did not run successfully.") + var credentialError = regexp.MustCompile("WARNING: 401 Error, Credentials not correct for") + var couldNotFindVersionError = regexp.MustCompile("Could not find a version that satisfies the requirement") + + switch { + case buildError.MatchString(cmdErr.Error()): + matches := buildError.FindAllStringSubmatch(cmdErr.Error(), 10) + dependencyName := "" + if len(matches) > 0 { + if len(matches[len(matches)-1]) > 1 { + dependencyName = "\"" + matches[len(matches)-1][1] + "\"" + } + } + cmdErr.SetDocumentation( + strings.Join( + []string{ + "Failed to build python dependency ", + dependencyName, + " with setup.py. This probably means the " + + "project was not set up correctly and " + + "could mean that an OS package is missing.", + }, ""), + ) + case credentialError.MatchString(cmdErr.Error()): + authErrDependencyNamePattern := regexp.MustCompile("No matching distribution found for ([^ ]*)\n") + dependencyNameMatch := authErrDependencyNamePattern.FindStringSubmatch(cmdErr.Error()) + dependencyName := "" + if len(dependencyNameMatch) > 1 { + dependencyName = "\"" + dependencyNameMatch[len(dependencyNameMatch)-1] + "\"" + } + cmdErr.SetDocumentation( + strings.Join( + []string{ + "Failed to install python dependency ", + dependencyName, + " due to authorization.\n" + util.InstallPrivateDependencyMessage, + }, ""), + ) + + case couldNotFindVersionError.MatchString(cmdErr.Error()): + dependencyNamePattern := regexp.MustCompile(`Could not find a version that satisfies the requirement ([\w=]+)`) + dependencyNameMatch := dependencyNamePattern.FindStringSubmatch(cmdErr.Error()) + dependencyName := "" + if len(dependencyNameMatch) > 1 { + dependency := strings.Split(dependencyNameMatch[1], "==") + dependencyName = "\"" + dependency[0] + "\"" + } + cmdErr.SetDocumentation( + strings.Join( + []string{ + "Failed to find a version that satisfies the requirement for python dependency ", + dependencyName, + ". This could mean that the package or version does not exist.\n" + util.InstallPrivateDependencyMessage, + }, ""), + ) } + j.Errors().Critical(cmdErr) +} - listCmdOutput, err := j.runListCmd() - if err != nil { - return err +func (j *Job) writeLockContent() job.IError { + status := "generating lock file" + j.SendStatus(status) + catCmdOutput, cmdErr := j.runCatCmd() + if cmdErr != nil { + cmdErr.SetStatus(status) + + return cmdErr + } + + listCmdOutput, cmdErr := j.runListCmd() + if cmdErr != nil { + cmdErr.SetStatus(status) + + return cmdErr } installedPackages := j.parsePipList(string(listCmdOutput)) - ShowCmdOutput, err := j.runShowCmd(installedPackages) - if err != nil { - return err + ShowCmdOutput, cmdErr := j.runShowCmd(installedPackages) + if cmdErr != nil { + cmdErr.SetStatus(status) + + return cmdErr } lockFileName := fmt.Sprintf("%s%s", filepath.Base(j.GetFile()), lockFileExtension) lockFile, err := j.fileWriter.Create(util.MakePathFromManifestFile(j.GetFile(), lockFileName)) if err != nil { - return err + cmdErr = util.NewPMJobError(err.Error()) + cmdErr.SetStatus(status) + + return cmdErr } defer closeFile(j, lockFile) @@ -131,30 +209,44 @@ func (j *Job) writeLockContent() error { fileContents = append(fileContents, string(ShowCmdOutput)) res := []byte(strings.Join(fileContents, "\n")) - j.SendStatus("writing lock file") + status = "writing lock file" + j.SendStatus(status) + err = j.fileWriter.Write(lockFile, res) + if err != nil { + cmdErr = util.NewPMJobError(err.Error()) + cmdErr.SetStatus(status) + + return cmdErr + } - return j.fileWriter.Write(lockFile, res) + return nil } -func (j *Job) runCreateVenvCmd() ([]byte, error) { +func (j *Job) runCreateVenvCmd() ([]byte, job.IError) { venvName := fmt.Sprintf("%s.venv", filepath.Base(j.GetFile())) fpath := filepath.Join(filepath.Dir(j.GetFile()), venvName) j.venvPath = fpath createVenvCmd, err := j.cmdFactory.MakeCreateVenvCmd(j.venvPath) if err != nil { - return nil, err + cmdErr := util.NewPMJobError(err.Error()) + cmdErr.SetCommand(createVenvCmd.String()) + + return nil, cmdErr } createVenvCmdOutput, err := createVenvCmd.Output() if err != nil { - return nil, j.GetExitError(err) + cmdErr := util.NewPMJobError(j.GetExitError(err).Error()) + cmdErr.SetCommand(createVenvCmd.String()) + + return nil, cmdErr } return createVenvCmdOutput, nil } -func (j *Job) runInstallCmd() ([]byte, error) { +func (j *Job) runInstallCmd() ([]byte, job.IError) { var command string if j.venvPath != "" { binDir := "bin" @@ -168,63 +260,88 @@ func (j *Job) runInstallCmd() ([]byte, error) { j.pipCommand = command installCmd, err := j.cmdFactory.MakeInstallCmd(j.pipCommand, j.GetFile()) if err != nil { - return nil, err + cmdErr := util.NewPMJobError(err.Error()) + cmdErr.SetCommand(installCmd.String()) + + return nil, cmdErr } installCmdOutput, err := installCmd.Output() if err != nil { - return nil, j.GetExitError(err) + cmdErr := util.NewPMJobError(j.GetExitError(err).Error()) + cmdErr.SetCommand(installCmd.String()) + + return nil, cmdErr } return installCmdOutput, nil } -func (j *Job) runCatCmd() ([]byte, error) { +func (j *Job) runCatCmd() ([]byte, job.IError) { listCmd, err := j.cmdFactory.MakeCatCmd(j.GetFile()) if err != nil { - return nil, err + cmdErr := util.NewPMJobError(err.Error()) + cmdErr.SetCommand(listCmd.String()) + + return nil, cmdErr } listCmdOutput, err := listCmd.Output() if err != nil { - return nil, j.GetExitError(err) + cmdErr := util.NewPMJobError(j.GetExitError(err).Error()) + cmdErr.SetCommand(listCmd.String()) + + return nil, cmdErr } return listCmdOutput, nil } -func (j *Job) runListCmd() ([]byte, error) { +func (j *Job) runListCmd() ([]byte, job.IError) { listCmd, err := j.cmdFactory.MakeListCmd(j.pipCommand) if err != nil { - return nil, err + cmdErr := util.NewPMJobError(err.Error()) + cmdErr.SetCommand(listCmd.String()) + + return nil, cmdErr } listCmdOutput, err := listCmd.Output() if err != nil { - return nil, j.GetExitError(err) + cmdErr := util.NewPMJobError(j.GetExitError(err).Error()) + cmdErr.SetCommand(listCmd.String()) + + return nil, cmdErr } return listCmdOutput, nil } -func (j *Job) runShowCmd(packages []string) ([]byte, error) { +func (j *Job) runShowCmd(packages []string) ([]byte, job.IError) { listCmd, err := j.cmdFactory.MakeShowCmd(j.pipCommand, packages) if err != nil { - return nil, err + cmdErr := util.NewPMJobError(err.Error()) + cmdErr.SetCommand(listCmd.String()) + + return nil, cmdErr } listCmdOutput, err := listCmd.Output() if err != nil { - return nil, j.GetExitError(err) + cmdErr := util.NewPMJobError(j.GetExitError(err).Error()) + cmdErr.SetCommand(listCmd.String()) + + return nil, cmdErr } return listCmdOutput, nil } -func closeFile(job *Job, file *os.File) { - err := job.fileWriter.Close(file) +func closeFile(j *Job, file *os.File) { + err := j.fileWriter.Close(file) if err != nil { - job.Errors().Critical(err) + jobError := util.NewPMJobError(err.Error()) + j.Errors().Critical(jobError) } } diff --git a/internal/resolution/pm/pip/job_test.go b/internal/resolution/pm/pip/job_test.go index 29d97378..1bb40af9 100644 --- a/internal/resolution/pm/pip/job_test.go +++ b/internal/resolution/pm/pip/job_test.go @@ -11,6 +11,7 @@ import ( jobTestdata "github.com/debricked/cli/internal/resolution/job/testdata" "github.com/debricked/cli/internal/resolution/pm/pip/testdata" + "github.com/debricked/cli/internal/resolution/pm/util" "github.com/debricked/cli/internal/resolution/pm/writer" writerTestdata "github.com/debricked/cli/internal/resolution/pm/writer/testdata" "github.com/stretchr/testify/assert" @@ -47,7 +48,9 @@ func TestRunCreateVenvCmdErr(t *testing.T) { j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), cmdErr) + correctErr := util.NewPMJobError(cmdErr.Error()) + correctErr.SetDocumentation("") + correctErr.SetStatus("") } func TestRunCreateVenvCmdOutputErr(t *testing.T) { @@ -72,7 +75,41 @@ func TestRunInstallCmdErr(t *testing.T) { j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), cmdErr) +} + +func TestRunInstallCmdErrors(t *testing.T) { + tests := []struct { + name string + err error + }{ + { + name: "Build Error", + err: errors.New(" python setup.py bdist_wheel did not run successfully. "), + }, + { + name: "Auth Error", + err: errors.New("WARNING: 401 Error, Credentials not correct for \n" + + "No matching distribution found for some-dependency>=0.1.3\n"), + }, + { + name: "Version Error", + err: errors.New("Could not find a version that satisfies the requirement test==123"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmdFactoryMock := testdata.NewEchoCmdFactory() + cmdFactoryMock.MakeInstallErr = tt.err + fileWriterMock := &writerTestdata.FileWriterMock{} + j := NewJob("file", true, cmdFactoryMock, fileWriterMock, pipCleaner{}) + + go jobTestdata.WaitStatus(j) + j.Run() + + assert.Len(t, j.Errors().GetAll(), 1) + }) + } } func TestRunInstallCmdOutputErr(t *testing.T) { @@ -97,7 +134,6 @@ func TestRunCatCmdErr(t *testing.T) { j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), cmdErr) } func TestRunCatCmdOutputErr(t *testing.T) { @@ -122,7 +158,6 @@ func TestRunListCmdErr(t *testing.T) { j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), cmdErr) } func TestRunListCmdOutputErr(t *testing.T) { @@ -147,7 +182,6 @@ func TestRunShowCmdErr(t *testing.T) { j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), cmdErr) } func TestRunShowCmdOutputErr(t *testing.T) { @@ -246,7 +280,6 @@ func TestRunCreateErr(t *testing.T) { j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), createErr) } func TestRunWriteErr(t *testing.T) { @@ -259,7 +292,6 @@ func TestRunWriteErr(t *testing.T) { j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), writeErr) } func TestRunCloseErr(t *testing.T) { @@ -272,7 +304,8 @@ func TestRunCloseErr(t *testing.T) { j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), closeErr) + correctErr := util.NewPMJobError(closeErr.Error()) + assert.Contains(t, j.Errors().GetAll(), correctErr) } type pipCleanerMock struct { @@ -294,7 +327,6 @@ func TestRunCleanErr(t *testing.T) { j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), CleanErr) } var wasCalled bool @@ -323,6 +355,5 @@ func TestErrorStillClean(t *testing.T) { j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), cmdErr) assert.True(t, wasCalled) } diff --git a/internal/resolution/pm/util/error.go b/internal/resolution/pm/util/error.go new file mode 100644 index 00000000..cccd8657 --- /dev/null +++ b/internal/resolution/pm/util/error.go @@ -0,0 +1,52 @@ +package util + +type PMJobError struct { + err string + cmd string + doc string + status string +} + +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." +var UnknownError = "No specific documentation for this problem yet. If you would like this message to more informative for this error, please create an issue here: https://github.com/debricked/cli/issues" + +func (e PMJobError) Error() string { + return e.err +} + +func (e PMJobError) Command() string { + if len(e.cmd) == 0 { + return "" + } + + return "`" + e.cmd + "`\n" +} + +func (e PMJobError) Documentation() string { + return e.doc + "\n" +} + +func (e PMJobError) Status() string { + return e.status +} + +func (e *PMJobError) SetStatus(status string) { + e.status = status +} + +func (e *PMJobError) SetDocumentation(doc string) { + e.doc = doc +} + +func (e *PMJobError) SetCommand(cmd string) { + e.cmd = cmd +} + +func NewPMJobError(err string) *PMJobError { + return &PMJobError{ + err: err, + cmd: "", + doc: UnknownError, + status: "", + } +} diff --git a/internal/resolution/pm/util/error_test.go b/internal/resolution/pm/util/error_test.go new file mode 100644 index 00000000..e213c519 --- /dev/null +++ b/internal/resolution/pm/util/error_test.go @@ -0,0 +1,86 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewError(t *testing.T) { + error_message := "error" + jobError := NewPMJobError(error_message) + assert.Equal(t, error_message, jobError.err) + assert.Equal(t, string(""), jobError.cmd) + assert.Equal( + t, + UnknownError, + jobError.doc, + ) + assert.Equal(t, string(""), jobError.status) + assert.NotNil(t, jobError) +} + +func TestPMJobErrorError(t *testing.T) { + jobError := PMJobError{ + err: "error", + cmd: "", + doc: "", + status: "", + } + assert.Equal(t, "error", jobError.Error()) +} + +func TestPMJobErrorCommand(t *testing.T) { + jobError := PMJobError{ + err: "", + cmd: "command", + doc: "", + status: "", + } + assert.Equal(t, "`command`\n", jobError.Command()) +} + +func TestPMJobErrorSetCommand(t *testing.T) { + jobError := NewPMJobError("") + assert.Equal(t, "", jobError.Command()) + jobError.SetCommand("command") + assert.Equal(t, "`command`\n", jobError.Command()) +} + +func TestPMJobErrorDocumentation(t *testing.T) { + jobError := PMJobError{ + err: "", + cmd: "", + doc: "documentation", + status: "", + } + assert.Equal(t, "documentation\n", jobError.Documentation()) +} + +func TestPMJobErrorSetDocumentation(t *testing.T) { + jobError := NewPMJobError("") + assert.Equal( + t, + string("No specific documentation for this problem yet. If you would like this message to more informative for this error, please create an issue here: https://github.com/debricked/cli/issues\n"), + jobError.Documentation(), + ) + jobError.SetDocumentation("documentation") + assert.Equal(t, "documentation\n", jobError.Documentation()) +} + +func TestPMJobErrorStatus(t *testing.T) { + jobError := PMJobError{ + err: "", + cmd: "", + doc: "", + status: "status", + } + assert.Equal(t, "status", jobError.Status()) +} + +func TestPMJobErrorSetStatus(t *testing.T) { + jobError := NewPMJobError("") + assert.Equal(t, "", jobError.Status()) + jobError.SetStatus("status") + assert.Equal(t, "status", jobError.Status()) +} diff --git a/internal/resolution/pm/util/util.go b/internal/resolution/pm/util/util.go index 3d620762..feb00c35 100644 --- a/internal/resolution/pm/util/util.go +++ b/internal/resolution/pm/util/util.go @@ -19,9 +19,9 @@ func MakePathFromManifestFile(siblingFile string, fileName string) string { return fmt.Sprintf("%s%s%s", dir, string(os.PathSeparator), fileName) } -func CloseFile(job job.IJob, fileWriter writer.IFileWriter, file *os.File) { +func CloseFile(j job.IJob, fileWriter writer.IFileWriter, file *os.File) { err := fileWriter.Close(file) if err != nil { - job.Errors().Critical(err) + j.Errors().Critical(NewPMJobError(err.Error())) } } diff --git a/internal/resolution/pm/util/util_test.go b/internal/resolution/pm/util/util_test.go index cfe951ae..72b73a37 100644 --- a/internal/resolution/pm/util/util_test.go +++ b/internal/resolution/pm/util/util_test.go @@ -48,6 +48,4 @@ func TestCloseFileErr(t *testing.T) { assert.True(t, j.Errors().HasError()) criticalErrs := j.Errors().GetCriticalErrors() assert.Len(t, criticalErrs, 1) - criticalErr := criticalErrs[0] - assert.ErrorIs(t, closeErr, criticalErr) } diff --git a/internal/resolution/pm/yarn/job.go b/internal/resolution/pm/yarn/job.go index 7f026a18..929b918f 100644 --- a/internal/resolution/pm/yarn/job.go +++ b/internal/resolution/pm/yarn/job.go @@ -2,6 +2,7 @@ package yarn import ( "github.com/debricked/cli/internal/resolution/job" + "github.com/debricked/cli/internal/resolution/pm/util" ) const ( @@ -37,7 +38,8 @@ func (j *Job) Run() { j.SendStatus("installing dependencies") _, err := j.runInstallCmd() if err != nil { - j.Errors().Critical(err) + jobError := util.NewPMJobError(err.Error()) + j.Errors().Critical(jobError) return } diff --git a/internal/resolution/pm/yarn/job_test.go b/internal/resolution/pm/yarn/job_test.go index 100cb987..47b5da48 100644 --- a/internal/resolution/pm/yarn/job_test.go +++ b/internal/resolution/pm/yarn/job_test.go @@ -5,6 +5,7 @@ import ( "testing" jobTestdata "github.com/debricked/cli/internal/resolution/job/testdata" + "github.com/debricked/cli/internal/resolution/pm/util" "github.com/debricked/cli/internal/resolution/pm/yarn/testdata" "github.com/stretchr/testify/assert" ) @@ -49,7 +50,7 @@ func TestRunInstallCmdErr(t *testing.T) { j.Run() assert.Len(t, j.Errors().GetAll(), 1) - assert.Contains(t, j.Errors().GetAll(), cmdErr) + assert.Contains(t, j.Errors().GetAll(), util.NewPMJobError(cmdErr.Error())) } func TestRunInstallCmdOutputErr(t *testing.T) { diff --git a/internal/resolution/resolution_test.go b/internal/resolution/resolution_test.go index 1062ca7a..f2bff9fa 100644 --- a/internal/resolution/resolution_test.go +++ b/internal/resolution/resolution_test.go @@ -1,7 +1,6 @@ package resolution import ( - "errors" "testing" "github.com/debricked/cli/internal/resolution/job" @@ -45,7 +44,7 @@ func TestHasError(t *testing.T) { assert.False(t, res.HasErr()) jobMock := testdata.NewJobMock("") - jobMock.SetErr(errors.New("error")) + jobMock.SetErr(job.NewBaseJobError("error")) res.jobs = append(res.jobs, jobMock) assert.True(t, res.HasErr()) } diff --git a/internal/resolution/resolver_test.go b/internal/resolution/resolver_test.go index 6cc59bc6..8bd73fad 100644 --- a/internal/resolution/resolver_test.go +++ b/internal/resolution/resolver_test.go @@ -187,7 +187,7 @@ func TestResolveHasResolutionErrs(t *testing.T) { groups.Add(file.Group{ManifestFile: goModFile}) f.SetGetGroupsReturnMock(groups, nil) - jobErr := errors.New("job-error") + jobErr := job.NewBaseJobError("job-error") jobWithErr := jobTestdata.NewJobMock(goModFile) jobWithErr.Errors().Warning(jobErr) schedulerMock := SchedulerMock{JobsMock: []job.IJob{jobWithErr}} diff --git a/internal/resolution/scheduler_test.go b/internal/resolution/scheduler_test.go index 737c1df3..35971429 100644 --- a/internal/resolution/scheduler_test.go +++ b/internal/resolution/scheduler_test.go @@ -1,7 +1,6 @@ package resolution import ( - "errors" "sort" "testing" @@ -70,7 +69,7 @@ func TestSchedule(t *testing.T) { func TestScheduleJobErr(t *testing.T) { s := NewScheduler(10) jobMock := testdata.NewJobMock("") - jobErr := errors.New("job-error") + jobErr := job.NewBaseJobError("job-error") jobMock.SetErr(jobErr) res, err := s.Schedule([]job.IJob{jobMock}) assert.NoError(t, err) diff --git a/internal/tui/resolution_error_list.go b/internal/tui/resolution_error_list.go index 336997bf..9a5fcab4 100644 --- a/internal/tui/resolution_error_list.go +++ b/internal/tui/resolution_error_list.go @@ -50,13 +50,33 @@ func (jobsErrList JobsErrorList) addJob(list *bytes.Buffer, job job.IJob) { for _, warning := range job.Errors().GetWarningErrors() { err := jobsErrList.createErrorString(warning, true) - jobString = fmt.Sprintf("* %s:\n\t%s\n", color.YellowString("Warning"), err) + cmd := warning.Command() + doc := warning.Documentation() + status := warning.Status() + jobString = fmt.Sprintf( + "* %s: %s failed\n%s\ncommand: %s\n\t%s\n", + color.YellowString("Warning"), + status, + color.BlueString(doc), + color.GreenString(cmd), + err, + ) list.Write([]byte(jobString)) } for _, critical := range job.Errors().GetCriticalErrors() { err := jobsErrList.createErrorString(critical, false) - jobString = fmt.Sprintf("* %s:\n\t%s\n", color.RedString("Critical"), err) + cmd := critical.Command() + doc := critical.Documentation() + status := critical.Status() + jobString = fmt.Sprintf( + "* %s: %s failed\n%s%s\t%s\n", + color.RedString("Critical"), + status, + color.BlueString(doc), + color.GreenString(cmd), + err, + ) list.Write([]byte(jobString)) } diff --git a/internal/tui/resolution_error_list_test.go b/internal/tui/resolution_error_list_test.go index 4912a384..a0e1c14a 100644 --- a/internal/tui/resolution_error_list_test.go +++ b/internal/tui/resolution_error_list_test.go @@ -2,7 +2,6 @@ package tui import ( "bytes" - "errors" "os" "testing" @@ -31,7 +30,7 @@ func TestRenderNoJobs(t *testing.T) { func TestRenderWarningJob(t *testing.T) { var listBuffer bytes.Buffer - warningErr := errors.New("warning-message") + warningErr := job.NewBaseJobError("warning-message") jobMock := testdata.NewJobMock("file") jobMock.Errors().Warning(warningErr) errList := NewJobsErrorList(&listBuffer, []job.IJob{jobMock}) @@ -53,7 +52,7 @@ func TestRenderWarningJob(t *testing.T) { func TestRenderCriticalJob(t *testing.T) { var listBuffer bytes.Buffer - warningErr := errors.New("critical-message") + warningErr := job.NewBaseJobError("critical-message") jobMock := testdata.NewJobMock("file") jobMock.Errors().Critical(warningErr) errList := NewJobsErrorList(&listBuffer, []job.IJob{jobMock}) @@ -77,10 +76,10 @@ func TestRenderCriticalAndWarningJob(t *testing.T) { jobMock := testdata.NewJobMock("manifest-file") - warningErr := errors.New("warning-message") + warningErr := job.NewBaseJobError("warning-message") jobMock.Errors().Warning(warningErr) - criticalErr := errors.New("critical-message") + criticalErr := job.NewBaseJobError("critical-message") jobMock.Errors().Critical(criticalErr) errList := NewJobsErrorList(&listBuffer, []job.IJob{jobMock}) @@ -106,7 +105,7 @@ func TestRenderCriticalAndWorkingJob(t *testing.T) { jobWithErrMock := testdata.NewJobMock("manifest-file") - criticalErr := errors.New("critical-message") + criticalErr := job.NewBaseJobError("critical-message") jobWithErrMock.Errors().Critical(criticalErr) jobWorkingMock := testdata.NewJobMock("working-manifest-file")