Add structured validation failures
This commit is contained in:
@@ -9,6 +9,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation"
|
||||
)
|
||||
|
||||
const testVersion = "v1.2.3"
|
||||
@@ -123,6 +125,14 @@ func TestDefaultValidationDoesNotInheritGOFLAGSRunFilter(t *testing.T) {
|
||||
if err == nil || !strings.Contains(output.String(), "TestExpectedFailure") {
|
||||
t.Fatalf("Check() error = %v, output = %q; wanted the failing standard test to run", err, output.String())
|
||||
}
|
||||
var failure *validation.Failure
|
||||
if !errors.As(err, &failure) || failure.Check != validation.GoTest {
|
||||
t.Fatalf("Check() error = %v; want a go-test validation failure", err)
|
||||
}
|
||||
var exitErr *exec.ExitError
|
||||
if !errors.As(err, &exitErr) {
|
||||
t.Fatalf("Check() error = %v; want the Go process exit error preserved", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
92
internal/validation/diagnostic.go
Normal file
92
internal/validation/diagnostic.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Check identifies one shared validation check.
|
||||
type Check string
|
||||
|
||||
const (
|
||||
// GoTest identifies the base Go test suite.
|
||||
GoTest Check = "go-test"
|
||||
)
|
||||
|
||||
// Finding describes a validation observation at an optional source location.
|
||||
type Finding struct {
|
||||
Path string
|
||||
Line int
|
||||
Column int
|
||||
Message string
|
||||
Target string
|
||||
Directive string
|
||||
}
|
||||
|
||||
// FileSizeMeasurement describes a measured file size and its selected limit.
|
||||
type FileSizeMeasurement struct {
|
||||
Bytes int64
|
||||
LimitBytes int64
|
||||
}
|
||||
|
||||
// Failure describes a failed shared validation check and preserves its cause.
|
||||
type Failure struct {
|
||||
Check Check
|
||||
Findings []Finding
|
||||
Measurement *FileSizeMeasurement
|
||||
Cause error
|
||||
}
|
||||
|
||||
// Error returns a useful summary of the failed check.
|
||||
func (failure *Failure) Error() string {
|
||||
return formatFailure(failure)
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying validation error.
|
||||
func (failure *Failure) Unwrap() error {
|
||||
if failure == nil {
|
||||
return nil
|
||||
}
|
||||
return failure.Cause
|
||||
}
|
||||
|
||||
func formatFailure(failure *Failure) string {
|
||||
if failure == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
var summary strings.Builder
|
||||
if failure.Check == "" {
|
||||
summary.WriteString("validation failed")
|
||||
} else {
|
||||
fmt.Fprintf(&summary, "%s validation failed", failure.Check)
|
||||
}
|
||||
for _, finding := range failure.Findings {
|
||||
summary.WriteString(": ")
|
||||
if finding.Path != "" {
|
||||
summary.WriteString(finding.Path)
|
||||
if finding.Line > 0 {
|
||||
fmt.Fprintf(&summary, ":%d", finding.Line)
|
||||
if finding.Column > 0 {
|
||||
fmt.Fprintf(&summary, ":%d", finding.Column)
|
||||
}
|
||||
}
|
||||
summary.WriteString(": ")
|
||||
}
|
||||
summary.WriteString(finding.Message)
|
||||
}
|
||||
if failure.Measurement != nil {
|
||||
fmt.Fprintf(&summary, ": measured %d bytes against a %d byte limit", failure.Measurement.Bytes, failure.Measurement.LimitBytes)
|
||||
}
|
||||
if failure.Cause != nil {
|
||||
if len(failure.Findings) == 0 && failure.Measurement == nil {
|
||||
summary.WriteString(": ")
|
||||
} else {
|
||||
summary.WriteString("; ")
|
||||
}
|
||||
summary.WriteString(failure.Cause.Error())
|
||||
}
|
||||
return summary.String()
|
||||
}
|
||||
|
||||
var _ error = (*Failure)(nil)
|
||||
var _ interface{ Unwrap() error } = (*Failure)(nil)
|
||||
49
internal/validation/diagnostic_test.go
Normal file
49
internal/validation/diagnostic_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRunPreservesCanceledGoSetup(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
err := Run(ctx, Request{RepositoryDir: t.TempDir()})
|
||||
var failure *Failure
|
||||
if !errors.As(err, &failure) {
|
||||
t.Fatalf("Run() error = %v, want a validation failure", err)
|
||||
}
|
||||
if failure.Check != GoTest {
|
||||
t.Fatalf("failure check = %q, want %q", failure.Check, GoTest)
|
||||
}
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Run() error = %v, want context cancellation preserved", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailureFormatsDiagnosticsAndUnwrapsCause(t *testing.T) {
|
||||
wantCause := errors.New("underlying check error")
|
||||
failure := &Failure{
|
||||
Check: GoTest,
|
||||
Findings: []Finding{{
|
||||
Path: "README.md",
|
||||
Line: 4,
|
||||
Column: 2,
|
||||
Message: "broken destination",
|
||||
}},
|
||||
Measurement: &FileSizeMeasurement{Bytes: 12, LimitBytes: 10},
|
||||
Cause: wantCause,
|
||||
}
|
||||
|
||||
if !errors.Is(failure, wantCause) {
|
||||
t.Fatalf("Failure does not unwrap its cause")
|
||||
}
|
||||
for _, part := range []string{"go-test", "README.md:4:2", "broken destination", "12 bytes", "10 byte limit", wantCause.Error()} {
|
||||
if !strings.Contains(failure.Error(), part) {
|
||||
t.Errorf("Failure.Error() = %q; want it to include %q", failure.Error(), part)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
||||
@@ -25,9 +24,12 @@ type Request struct {
|
||||
func Run(ctx context.Context, request Request) error {
|
||||
tool, err := gotool.New(ctx, request.RepositoryDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("prepare Go toolchain: %w", err)
|
||||
return &Failure{Check: GoTest, Cause: err}
|
||||
}
|
||||
_, err = tool.Run(ctx, request.RepositoryDir, gotool.Native, gotool.Target{}, request.Output,
|
||||
"test", "-mod=readonly", "-count=1", "./...")
|
||||
return err
|
||||
if err != nil {
|
||||
return &Failure{Check: GoTest, Cause: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user