Add structured validation failures
This commit is contained in:
@@ -46,7 +46,7 @@ these implemented packages:
|
|||||||
| `internal/tempdir` | Disposable system temporary storage outside source worktrees. |
|
| `internal/tempdir` | Disposable system temporary storage outside source worktrees. |
|
||||||
| `internal/process` | Cancellable child-process execution with per-command environment overlays and removals. |
|
| `internal/process` | Cancellable child-process execution with per-command environment overlays and removals. |
|
||||||
| `internal/preparation` | Coordination for expected-commit tag checkout and Go module dependency preparation. |
|
| `internal/preparation` | Coordination for expected-commit tag checkout and Go module dependency preparation. |
|
||||||
| `internal/validation` | Shared default Go validation for captured candidates; `internal/candidate` retains capture, callback sequencing, and local-state rechecks. |
|
| `internal/validation` | Shared default Go validation and structured check failures for captured candidates; `internal/candidate` retains capture, callback sequencing, and local-state rechecks. |
|
||||||
| `internal/publish` | Publication guards, one validation run, annotated tag creation, push, and recovery state. |
|
| `internal/publish` | Publication guards, one validation run, annotated tag creation, push, and recovery state. |
|
||||||
| `internal/runtimeinfo` | Private version lookup used by the runtime facade. |
|
| `internal/runtimeinfo` | Private version lookup used by the runtime facade. |
|
||||||
| `internal/versioncontract` | Shared stable-version rules and linker-symbol identity. |
|
| `internal/versioncontract` | Shared stable-version rules and linker-symbol identity. |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Validation model implementation plan
|
# Validation model implementation plan
|
||||||
|
|
||||||
Status: In progress; the shared validation executor is complete.
|
Status: In progress; the shared validation executor and internal diagnostics are complete.
|
||||||
|
|
||||||
This plan implements the enabling work in [model.md](model.md), guided by
|
This plan implements the enabling work in [model.md](model.md), guided by
|
||||||
[ADR 0003](../adr/0003-declarative-validation.md). It is written for a Luna
|
[ADR 0003](../adr/0003-declarative-validation.md). It is written for a Luna
|
||||||
@@ -206,6 +206,10 @@ inventory to reflect the implemented executor.
|
|||||||
|
|
||||||
## Stage 2: Introduce internal validation diagnostics
|
## Stage 2: Introduce internal validation diagnostics
|
||||||
|
|
||||||
|
**Completion:** Complete. Shared Go setup and test failures carry the internal
|
||||||
|
`go-test` identity and preserve their underlying errors, including cancellation
|
||||||
|
and subprocess exit errors.
|
||||||
|
|
||||||
**Outcome:** The executor returns a small structured failure with an underlying
|
**Outcome:** The executor returns a small structured failure with an underlying
|
||||||
cause, independently of public error conversion.
|
cause, independently of public error conversion.
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation"
|
||||||
)
|
)
|
||||||
|
|
||||||
const testVersion = "v1.2.3"
|
const testVersion = "v1.2.3"
|
||||||
@@ -123,6 +125,14 @@ func TestDefaultValidationDoesNotInheritGOFLAGSRunFilter(t *testing.T) {
|
|||||||
if err == nil || !strings.Contains(output.String(), "TestExpectedFailure") {
|
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())
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
||||||
@@ -25,9 +24,12 @@ type Request struct {
|
|||||||
func Run(ctx context.Context, request Request) error {
|
func Run(ctx context.Context, request Request) error {
|
||||||
tool, err := gotool.New(ctx, request.RepositoryDir)
|
tool, err := gotool.New(ctx, request.RepositoryDir)
|
||||||
if err != nil {
|
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,
|
_, err = tool.Run(ctx, request.RepositoryDir, gotool.Native, gotool.Target{}, request.Output,
|
||||||
"test", "-mod=readonly", "-count=1", "./...")
|
"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