Expose validation errors through the root API
This commit is contained in:
2
check.go
2
check.go
@@ -51,7 +51,7 @@ func Check(ctx context.Context, request ReleaseRequest) (Candidate, error) {
|
|||||||
Output: request.Output,
|
Output: request.Output,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Candidate{}, err
|
return Candidate{}, convertValidationError(err)
|
||||||
}
|
}
|
||||||
return Candidate{
|
return Candidate{
|
||||||
RepositoryDir: checked.RepositoryDir,
|
RepositoryDir: checked.RepositoryDir,
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ not a separate release matrix:
|
|||||||
| Capability | Implementation owner | Existing evidence |
|
| Capability | Implementation owner | Existing evidence |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Runtime version lookup and injected application version | `buildinfo`, `internal/runtimeinfo`, `internal/versioncontract`, `internal/build` | `TestVersionInConsumerExecutable`, `TestVersion` |
|
| Runtime version lookup and injected application version | `buildinfo`, `internal/runtimeinfo`, `internal/versioncontract`, `internal/build` | `TestVersionInConsumerExecutable`, `TestVersion` |
|
||||||
| Candidate capture, shared validation, additional checks, and rechecks | `releasekit.Check`, `releasekit.Publish`, `internal/candidate`, `internal/validation`, `cmd/releasekit/internal/cli` | `TestCheckCapturesCandidateAndCallsAdditionalChecksOnce`, `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestPublishFailedStandardSuiteSkipsAdditionalChecksAndTagCreation`, `TestPublishAdditionalCheckFailurePreservesCauseAndPreventsTagCreation`, `TestCheckCommandBindsConfiguredChecksToCapturedCandidate`, `TestPublishCommandPublishesConfiguredCandidate` |
|
| Candidate capture, shared validation, public diagnostics, additional checks, and rechecks | `releasekit.Check`, `releasekit.Publish`, `internal/candidate`, `internal/validation`, `cmd/releasekit/internal/cli` | `TestCheckCapturesCandidateAndCallsAdditionalChecksOnce`, `TestCheckReturnsPublicValidationError`, `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestPublishFailedStandardSuiteSkipsAdditionalChecksAndTagCreation`, `TestPublishAdditionalCheckFailurePreservesCauseAndPreventsTagCreation`, `TestCheckCommandBindsConfiguredChecksToCapturedCandidate`, `TestPublishCommandPublishesConfiguredCandidate` |
|
||||||
| Shared Go settings for validation and builds | `internal/gotool`, `internal/process`, `internal/candidate`, `internal/build` | `TestDefaultValidationDoesNotInheritGOFLAGSRunFilter`, `TestNativeAndStaticCommandsUseOwnedBaseline`, `TestPersistentSettingsPreserveCachesAndNativeCTools`, `TestBuildIgnoresAmbientAndPersistentArchitectureTuning` |
|
| Shared Go settings for validation and builds | `internal/gotool`, `internal/process`, `internal/candidate`, `internal/build` | `TestDefaultValidationDoesNotInheritGOFLAGSRunFilter`, `TestNativeAndStaticCommandsUseOwnedBaseline`, `TestPersistentSettingsPreserveCachesAndNativeCTools`, `TestBuildIgnoresAmbientAndPersistentArchitectureTuning` |
|
||||||
| Authoritative endpoint and exact annotated tag publication | `releasekit.Publish`, `internal/publish`, `internal/git` | `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestResolveUsesPushURLAndQueriesWithoutMutation` |
|
| Authoritative endpoint and exact annotated tag publication | `releasekit.Publish`, `internal/publish`, `internal/git` | `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestResolveUsesPushURLAndQueriesWithoutMutation` |
|
||||||
| Publication partial and uncertain recovery | `internal/publish`, `internal/git` | `TestPublishRejectedPushKeepsLocalTagAndReportsNotPublished`, `TestPublishLostAcknowledgmentConfirmsExactRemoteObject`, `TestPublishLostAcknowledgmentWithChangedEndpointRemainsUnknown`, `TestPublishUnavailableReconciliationRemainsUnknownAndPreservesPushError` |
|
| Publication partial and uncertain recovery | `internal/publish`, `internal/git` | `TestPublishRejectedPushKeepsLocalTagAndReportsNotPublished`, `TestPublishLostAcknowledgmentConfirmsExactRemoteObject`, `TestPublishLostAcknowledgmentWithChangedEndpointRemainsUnknown`, `TestPublishUnavailableReconciliationRemainsUnknownAndPreservesPushError` |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Validation model implementation plan
|
# Validation model implementation plan
|
||||||
|
|
||||||
Status: In progress; the shared executor, diagnostics, committed-file reader, and destination resolver are complete.
|
Status: In progress; shared execution, committed-file inspection, destination resolution, and facade 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
|
||||||
@@ -280,6 +280,10 @@ syntax, anchors, and selected failure policy.
|
|||||||
|
|
||||||
## Stage 5: Add facade declarations and shared request conversion
|
## Stage 5: Add facade declarations and shared request conversion
|
||||||
|
|
||||||
|
**Completion:** Complete. The root facade exposes its own validation
|
||||||
|
diagnostic types, and both `Check` and `Publish` convert shared failures while
|
||||||
|
preserving candidate context and the original error chain.
|
||||||
|
|
||||||
**Outcome:** The public model has an agreed representation, shared conversion,
|
**Outcome:** The public model has an agreed representation, shared conversion,
|
||||||
and cause-preserving errors across `Check` and `Publish`.
|
and cause-preserving errors across `Check` and `Publish`.
|
||||||
|
|
||||||
|
|||||||
@@ -70,10 +70,91 @@ func checkRepository(ctx context.Context, dir string) error {
|
|||||||
if violations := checkExports(pkg); len(violations) != 0 {
|
if violations := checkExports(pkg); len(violations) != 0 {
|
||||||
return fmt.Errorf("facade %s has invalid public contracts:\n %s", path, strings.Join(violations, "\n "))
|
return fmt.Errorf("facade %s has invalid public contracts:\n %s", path, strings.Join(violations, "\n "))
|
||||||
}
|
}
|
||||||
|
if path == modulePath {
|
||||||
|
if violations := checkValidationTypes(pkg); len(violations) != 0 {
|
||||||
|
return fmt.Errorf("facade %s has invalid validation diagnostics:\n %s", path, strings.Join(violations, "\n "))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkValidationTypes(pkg *types.Package) []string {
|
||||||
|
var violations []string
|
||||||
|
for _, name := range []string{"ValidationCheck", "ValidationFinding", "FileSizeMeasurement", "ValidationError"} {
|
||||||
|
object, ok := pkg.Scope().Lookup(name).(*types.TypeName)
|
||||||
|
if !ok {
|
||||||
|
violations = append(violations, fmt.Sprintf("missing facade-owned type %s", name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
named, ok := types.Unalias(object.Type()).(*types.Named)
|
||||||
|
if !ok || named.Obj().Pkg() == nil || named.Obj().Pkg().Path() != pkg.Path() {
|
||||||
|
violations = append(violations, fmt.Sprintf("%s is not a facade-owned named type", name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
validationError, ok := pkg.Scope().Lookup("ValidationError").(*types.TypeName)
|
||||||
|
if !ok {
|
||||||
|
return violations
|
||||||
|
}
|
||||||
|
named, ok := types.Unalias(validationError.Type()).(*types.Named)
|
||||||
|
if !ok {
|
||||||
|
return violations
|
||||||
|
}
|
||||||
|
structure, ok := named.Underlying().(*types.Struct)
|
||||||
|
if !ok {
|
||||||
|
return append(violations, "ValidationError is not a struct")
|
||||||
|
}
|
||||||
|
fields := map[string]string{
|
||||||
|
"Check": "ValidationCheck",
|
||||||
|
"Findings": "[]ValidationFinding",
|
||||||
|
"Measurement": "*FileSizeMeasurement",
|
||||||
|
}
|
||||||
|
for fieldName, expected := range fields {
|
||||||
|
fieldIndex := -1
|
||||||
|
for index := 0; index < structure.NumFields(); index++ {
|
||||||
|
if structure.Field(index).Name() == fieldName && structure.Field(index).Exported() {
|
||||||
|
fieldIndex = index
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fieldIndex < 0 || !validationFieldMatches(structure.Field(fieldIndex).Type(), expected, pkg.Path()) {
|
||||||
|
violations = append(violations, fmt.Sprintf("ValidationError.%s must expose %s", fieldName, expected))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cause := fieldByName(structure, "Cause"); cause == nil || !cause.Exported() || !types.Identical(cause.Type(), types.Universe.Lookup("error").Type()) {
|
||||||
|
violations = append(violations, "ValidationError.Cause must expose error")
|
||||||
|
}
|
||||||
|
return violations
|
||||||
|
}
|
||||||
|
|
||||||
|
func fieldByName(structure *types.Struct, name string) *types.Var {
|
||||||
|
for index := 0; index < structure.NumFields(); index++ {
|
||||||
|
if structure.Field(index).Name() == name {
|
||||||
|
return structure.Field(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validationFieldMatches(fieldType types.Type, expected, packagePath string) bool {
|
||||||
|
switch expected {
|
||||||
|
case "[]ValidationFinding":
|
||||||
|
slice, ok := types.Unalias(fieldType).(*types.Slice)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
fieldType = slice.Elem()
|
||||||
|
case "*FileSizeMeasurement":
|
||||||
|
pointer, ok := types.Unalias(fieldType).(*types.Pointer)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
fieldType = pointer.Elem()
|
||||||
|
}
|
||||||
|
named, ok := types.Unalias(fieldType).(*types.Named)
|
||||||
|
return ok && named.Obj().Pkg() != nil && named.Obj().Pkg().Path() == packagePath && named.Obj().Name() == strings.TrimLeft(expected, "*[]")
|
||||||
|
}
|
||||||
|
|
||||||
func listPackages(ctx context.Context, dir string) ([]listedPackage, error) {
|
func listPackages(ctx context.Context, dir string) ([]listedPackage, error) {
|
||||||
cmd := exec.CommandContext(ctx, "go", "list", "-deps", "-export", "-json", "./...")
|
cmd := exec.CommandContext(ctx, "go", "list", "-deps", "-export", "-json", "./...")
|
||||||
cmd.Dir = dir
|
cmd.Dir = dir
|
||||||
|
|||||||
@@ -101,6 +101,35 @@ func Open(context.Context) *Handle { return nil }
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidationDiagnosticTypesBelongToFacade(t *testing.T) {
|
||||||
|
valid := `package facade
|
||||||
|
type ValidationCheck string
|
||||||
|
type ValidationFinding struct{}
|
||||||
|
type FileSizeMeasurement struct{}
|
||||||
|
type ValidationError struct {
|
||||||
|
Check ValidationCheck
|
||||||
|
Findings []ValidationFinding
|
||||||
|
Measurement *FileSizeMeasurement
|
||||||
|
Cause error
|
||||||
|
}
|
||||||
|
`
|
||||||
|
pkg := typecheck(t, token.NewFileSet(), modulePath, valid)
|
||||||
|
if violations := checkValidationTypes(pkg); len(violations) != 0 {
|
||||||
|
t.Fatalf("checkValidationTypes() = %v, want facade-owned diagnostics", violations)
|
||||||
|
}
|
||||||
|
|
||||||
|
implementation := typecheck(t, token.NewFileSet(), "example.com/implementation", `package implementation
|
||||||
|
type State string
|
||||||
|
`)
|
||||||
|
fset := token.NewFileSet()
|
||||||
|
fixtureImporter := singlePackageImporter{path: "example.com/implementation", pkg: implementation}
|
||||||
|
invalid := strings.Replace(valid, "type ValidationCheck string", "import \"example.com/implementation\"\ntype ValidationCheck = implementation.State", 1)
|
||||||
|
pkg = typecheckWithImporter(t, fset, modulePath, invalid, fixtureImporter)
|
||||||
|
if violations := checkValidationTypes(pkg); !strings.Contains(strings.Join(violations, "\n"), "ValidationCheck is not a facade-owned named type") {
|
||||||
|
t.Fatalf("checkValidationTypes() = %v, want ownership violation", violations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestActualRepositoryBoundaries(t *testing.T) {
|
func TestActualRepositoryBoundaries(t *testing.T) {
|
||||||
if err := checkRepository(t.Context(), "../.."); err != nil {
|
if err := checkRepository(t.Context(), "../.."); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|||||||
@@ -76,5 +76,5 @@ func Publish(ctx context.Context, request ReleaseRequest) (PublicationResult, er
|
|||||||
Phase: PublicationPhase(result.Phase),
|
Phase: PublicationPhase(result.Phase),
|
||||||
TagObject: result.TagObject,
|
TagObject: result.TagObject,
|
||||||
State: PublicationState(result.State),
|
State: PublicationState(result.State),
|
||||||
}, err
|
}, convertValidationError(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,17 @@ func TestPublishFailedStandardSuiteSkipsAdditionalChecksAndTagCreation(t *testin
|
|||||||
if err == nil || !strings.Contains(err.Error(), "fixture standard suite failure") {
|
if err == nil || !strings.Contains(err.Error(), "fixture standard suite failure") {
|
||||||
t.Fatalf("Publish() error = %v, want fixture standard suite failure", err)
|
t.Fatalf("Publish() error = %v, want fixture standard suite failure", err)
|
||||||
}
|
}
|
||||||
|
var validationError *releasekit.ValidationError
|
||||||
|
if !errors.As(err, &validationError) || validationError.Check != "go-test" {
|
||||||
|
t.Fatalf("Publish() error = %v, want a public go-test validation error", err)
|
||||||
|
}
|
||||||
|
var exitErr *exec.ExitError
|
||||||
|
if !errors.As(err, &exitErr) {
|
||||||
|
t.Fatalf("Publish() error = %v, want the Go process error preserved", err)
|
||||||
|
}
|
||||||
|
if !errors.Is(err, exitErr) {
|
||||||
|
t.Fatal("Publish() error does not preserve the Go process error identity")
|
||||||
|
}
|
||||||
if checks != 0 || result.TagObject != "" || result.State != releasekit.PublicationNotAttempted {
|
if checks != 0 || result.TagObject != "" || result.State != releasekit.PublicationNotAttempted {
|
||||||
t.Fatalf("failed standard suite result=%#v additional checks=%d", result, checks)
|
t.Fatalf("failed standard suite result=%#v additional checks=%d", result, checks)
|
||||||
}
|
}
|
||||||
@@ -122,6 +133,27 @@ func TestPublishFailedStandardSuiteSkipsAdditionalChecksAndTagCreation(t *testin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCheckReturnsPublicValidationError(t *testing.T) {
|
||||||
|
repo := newPublishFixture(t)
|
||||||
|
t.Setenv("RELEASEKIT_FAIL_FIXTURE_SUITE", "1")
|
||||||
|
_, err := releasekit.Check(context.Background(), releasekit.ReleaseRequest{
|
||||||
|
RepositoryDir: repo.work,
|
||||||
|
Product: "Sample App",
|
||||||
|
Version: "v1.2.3",
|
||||||
|
})
|
||||||
|
var validationError *releasekit.ValidationError
|
||||||
|
if !errors.As(err, &validationError) || validationError.Check != "go-test" {
|
||||||
|
t.Fatalf("Check() error = %v, want a public go-test validation error", err)
|
||||||
|
}
|
||||||
|
var exitErr *exec.ExitError
|
||||||
|
if !errors.As(err, &exitErr) {
|
||||||
|
t.Fatalf("Check() error = %v, want the Go process error preserved", err)
|
||||||
|
}
|
||||||
|
if !errors.Is(err, exitErr) {
|
||||||
|
t.Fatal("Check() error does not preserve the Go process error identity")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPublishAdditionalCheckFailurePreservesCauseAndPreventsTagCreation(t *testing.T) {
|
func TestPublishAdditionalCheckFailurePreservesCauseAndPreventsTagCreation(t *testing.T) {
|
||||||
repo := newPublishFixture(t)
|
repo := newPublishFixture(t)
|
||||||
suiteRuns := filepath.Join(t.TempDir(), "suite-runs")
|
suiteRuns := filepath.Join(t.TempDir(), "suite-runs")
|
||||||
@@ -140,6 +172,10 @@ func TestPublishAdditionalCheckFailurePreservesCauseAndPreventsTagCreation(t *te
|
|||||||
if !errors.Is(err, wantErr) {
|
if !errors.Is(err, wantErr) {
|
||||||
t.Fatalf("Publish() error = %v, want wrapped additional-check error", err)
|
t.Fatalf("Publish() error = %v, want wrapped additional-check error", err)
|
||||||
}
|
}
|
||||||
|
var validationError *releasekit.ValidationError
|
||||||
|
if errors.As(err, &validationError) {
|
||||||
|
t.Fatalf("Publish() callback error was reclassified as validation failure: %v", err)
|
||||||
|
}
|
||||||
if runs, readErr := os.ReadFile(suiteRuns); readErr != nil || string(runs) != "ran\n" {
|
if runs, readErr := os.ReadFile(suiteRuns); readErr != nil || string(runs) != "ran\n" {
|
||||||
t.Fatalf("standard suite runs = %q, error = %v; want one successful run", runs, readErr)
|
t.Fatalf("standard suite runs = %q, error = %v; want one successful run", runs, readErr)
|
||||||
}
|
}
|
||||||
|
|||||||
121
validation_error.go
Normal file
121
validation_error.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package releasekit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidationCheck identifies a shared validation check that failed.
|
||||||
|
// Current checks include "go-test".
|
||||||
|
type ValidationCheck string
|
||||||
|
|
||||||
|
// ValidationFinding describes a failed check observation at an optional
|
||||||
|
// repository-relative source location.
|
||||||
|
type ValidationFinding struct {
|
||||||
|
Path string // repository-relative path when available
|
||||||
|
Line int // one-based source line, or zero when unavailable
|
||||||
|
Column int // one-based source column, or zero when unavailable
|
||||||
|
Message string // useful detail that is not promised to have exact wording
|
||||||
|
Target string // applicable target for a file check
|
||||||
|
Directive string // applicable directive for a file check
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileSizeMeasurement reports a measured file size and its selected limit.
|
||||||
|
type FileSizeMeasurement struct {
|
||||||
|
Bytes int64
|
||||||
|
LimitBytes int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidationError describes a failed shared validation check returned by
|
||||||
|
// [Check] or [Publish]. Cause preserves the contextual and underlying error chain.
|
||||||
|
type ValidationError struct {
|
||||||
|
Check ValidationCheck // failed shared check
|
||||||
|
Findings []ValidationFinding // actionable observations, when available
|
||||||
|
Measurement *FileSizeMeasurement // measured file size, when applicable
|
||||||
|
Cause error // original contextual and underlying error chain
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns the contextual validation error summary.
|
||||||
|
func (failure *ValidationError) Error() string {
|
||||||
|
if failure == nil {
|
||||||
|
return "<nil>"
|
||||||
|
}
|
||||||
|
if failure.Cause != nil {
|
||||||
|
return failure.Cause.Error()
|
||||||
|
}
|
||||||
|
return formatPublicValidationError(failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap returns the original validation error chain.
|
||||||
|
func (failure *ValidationError) Unwrap() error {
|
||||||
|
if failure == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return failure.Cause
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertValidationError(err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var failure *validation.Failure
|
||||||
|
if !errors.As(err, &failure) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result := &ValidationError{
|
||||||
|
Check: ValidationCheck(failure.Check),
|
||||||
|
Cause: err,
|
||||||
|
}
|
||||||
|
if len(failure.Findings) != 0 {
|
||||||
|
result.Findings = make([]ValidationFinding, len(failure.Findings))
|
||||||
|
for index, finding := range failure.Findings {
|
||||||
|
result.Findings[index] = ValidationFinding{
|
||||||
|
Path: finding.Path,
|
||||||
|
Line: finding.Line,
|
||||||
|
Column: finding.Column,
|
||||||
|
Message: finding.Message,
|
||||||
|
Target: finding.Target,
|
||||||
|
Directive: finding.Directive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if failure.Measurement != nil {
|
||||||
|
result.Measurement = &FileSizeMeasurement{
|
||||||
|
Bytes: failure.Measurement.Bytes,
|
||||||
|
LimitBytes: failure.Measurement.LimitBytes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatPublicValidationError(failure *ValidationError) string {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
return summary.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ error = (*ValidationError)(nil)
|
||||||
Reference in New Issue
Block a user