Complete comparison failure summaries
This commit is contained in:
@@ -237,7 +237,7 @@ func TestCompareCommandReportsCommittedBundleWhenCleanupFails(t *testing.T) {
|
||||
if err := json.Unmarshal(stdout.Bytes(), &summary); err != nil {
|
||||
t.Fatalf("decode summary: %v\n%s", err, stdout.String())
|
||||
}
|
||||
if summary.Status != summaryStatusFailed || summary.Error == nil || summary.Error.Message != "comparison did not complete" || summary.ManifestPath != result.ManifestPath || summary.DataPackagePath != result.DataPackagePath || summary.Results[0].ReportPath == "" {
|
||||
if summary.Status != summaryStatusFailed || summary.Error == nil || summary.Error.Category != "publication_cleanup" || summary.Error.Message != "comparison published but cleanup did not complete" || summary.ManifestPath != result.ManifestPath || summary.DataPackagePath != result.DataPackagePath || summary.Results[0].ReportPath == "" {
|
||||
t.Fatalf("summary = %#v", summary)
|
||||
}
|
||||
for _, unsafe := range []string{cleanupCause.Error(), backupPath} {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/app"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/comparison"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptexec"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||
@@ -216,29 +217,50 @@ func comparisonSummaryStatus(result *app.ComparisonResult, err error) string {
|
||||
}
|
||||
|
||||
func safeComparisonSummaryError(err error) *comparison.SafeError {
|
||||
message := "comparison did not complete"
|
||||
if aggregate, ok := comparisonAggregateErrorMessage(err.Error()); ok {
|
||||
message = aggregate
|
||||
} else if errors.Is(err, context.DeadlineExceeded) {
|
||||
message = "comparison deadline exceeded"
|
||||
} else if errors.Is(err, context.Canceled) {
|
||||
message = "comparison canceled"
|
||||
if aggregate, ok := comparisonAggregateErrorMessage(err); ok {
|
||||
safe := comparison.NewSafeError("application", aggregate)
|
||||
return &safe
|
||||
}
|
||||
safe := comparison.NewSafeError("application", message)
|
||||
var cleanupErr *comparison.PublicationCleanupError
|
||||
if errors.As(err, &cleanupErr) {
|
||||
safe := comparison.NewSafeError("publication_cleanup", "comparison published but cleanup did not complete")
|
||||
return &safe
|
||||
}
|
||||
var destinationErr *comparison.DestinationError
|
||||
if errors.As(err, &destinationErr) {
|
||||
safe := comparison.NewSafeError("destination_"+string(destinationErr.Kind), "comparison destination preflight failed")
|
||||
return &safe
|
||||
}
|
||||
if category := promptexec.CategoryOf(err); category != "" {
|
||||
safe := comparison.NewSafeError(string(category), "comparison prompt operation failed")
|
||||
return &safe
|
||||
}
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
safe := comparison.NewSafeError("deadline_exceeded", "comparison deadline exceeded")
|
||||
return &safe
|
||||
}
|
||||
if errors.Is(err, context.Canceled) {
|
||||
safe := comparison.NewSafeError("canceled", "comparison canceled")
|
||||
return &safe
|
||||
}
|
||||
safe := comparison.NewSafeError("application", "comparison did not complete")
|
||||
return &safe
|
||||
}
|
||||
|
||||
func comparisonAggregateErrorMessage(value string) (string, bool) {
|
||||
func comparisonAggregateErrorMessage(err error) (string, bool) {
|
||||
const prefix = "comparison completed with "
|
||||
const suffix = " failed profiles"
|
||||
if !strings.HasPrefix(value, prefix) || !strings.HasSuffix(value, suffix) {
|
||||
return "", false
|
||||
for candidate := err; candidate != nil; candidate = errors.Unwrap(candidate) {
|
||||
value := candidate.Error()
|
||||
if !strings.HasPrefix(value, prefix) || !strings.HasSuffix(value, suffix) {
|
||||
continue
|
||||
}
|
||||
count, parseErr := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(value, prefix), suffix))
|
||||
if parseErr == nil && count > 0 {
|
||||
return fmt.Sprintf("comparison completed with %d failed profiles", count), true
|
||||
}
|
||||
}
|
||||
count, err := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(value, prefix), suffix))
|
||||
if err != nil || count < 1 {
|
||||
return "", false
|
||||
}
|
||||
return fmt.Sprintf("comparison completed with %d failed profiles", count), true
|
||||
return "", false
|
||||
}
|
||||
|
||||
func batchSummaryStatus(result *app.BatchResult) string {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -135,3 +136,76 @@ func TestComparisonSummaryClassifiesCompleteAndAllFailedResults(t *testing.T) {
|
||||
t.Fatalf("all-failed summary = %#v", summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeComparisonSummaryErrorClassifiesWrappedFailures(t *testing.T) {
|
||||
unsafeDetail := "unsafe filesystem and provider detail"
|
||||
unsafeCause := errors.New(unsafeDetail)
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
err error
|
||||
category string
|
||||
message string
|
||||
}{
|
||||
{
|
||||
name: "aggregate profile failure",
|
||||
err: fmt.Errorf("outer wrapper: %w", errors.New("comparison completed with 2 failed profiles")),
|
||||
category: "application",
|
||||
message: "comparison completed with 2 failed profiles",
|
||||
},
|
||||
{
|
||||
name: "canceled",
|
||||
err: fmt.Errorf("outer wrapper: %w", context.Canceled),
|
||||
category: "canceled",
|
||||
message: "comparison canceled",
|
||||
},
|
||||
{
|
||||
name: "deadline exceeded",
|
||||
err: fmt.Errorf("outer wrapper: %w", context.DeadlineExceeded),
|
||||
category: "deadline_exceeded",
|
||||
message: "comparison deadline exceeded",
|
||||
},
|
||||
{
|
||||
name: "prompt operation",
|
||||
err: fmt.Errorf("outer wrapper: %w", promptexec.NewError(promptexec.Generation, "unsafe prompt detail", fmt.Errorf("%w: %s", context.Canceled, unsafeDetail))),
|
||||
category: string(promptexec.Generation),
|
||||
message: "comparison prompt operation failed",
|
||||
},
|
||||
{
|
||||
name: "destination preflight",
|
||||
err: fmt.Errorf("outer wrapper: %w", &comparison.DestinationError{
|
||||
Kind: comparison.DestinationNotEmpty, Target: "/tmp/" + unsafeDetail, Err: fmt.Errorf("%w: %s", context.Canceled, unsafeDetail),
|
||||
}),
|
||||
category: "destination_not_empty",
|
||||
message: "comparison destination preflight failed",
|
||||
},
|
||||
{
|
||||
name: "publication cleanup",
|
||||
err: fmt.Errorf("outer wrapper: %w", &comparison.PublicationCleanupError{
|
||||
RetainedBackupPath: "/tmp/" + unsafeDetail, Err: fmt.Errorf("%w: %s", context.Canceled, unsafeDetail),
|
||||
}),
|
||||
category: "publication_cleanup",
|
||||
message: "comparison published but cleanup did not complete",
|
||||
},
|
||||
{
|
||||
name: "unknown",
|
||||
err: fmt.Errorf("outer wrapper: %w", unsafeCause),
|
||||
category: "application",
|
||||
message: "comparison did not complete",
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := &app.ComparisonResult{ComparisonID: "comparison_test", ReportID: report.Daily}
|
||||
summary := newComparisonSummary(result, test.err)
|
||||
if summary.Error == nil || summary.Error.Category != test.category || summary.Error.Message != test.message {
|
||||
t.Fatalf("summary error = %#v, want %q/%q", summary.Error, test.category, test.message)
|
||||
}
|
||||
data, err := json.Marshal(summary)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(data), unsafeDetail) {
|
||||
t.Fatalf("summary includes unsafe detail: %s", data)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user