137 lines
7.7 KiB
Go
137 lines
7.7 KiB
Go
package ckconfig
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
|
|
commandkit "go.maximumdirect.net/commandkit"
|
|
configkit "go.maximumdirect.net/configkit"
|
|
)
|
|
|
|
func validationDiagnostic(message string) commandkit.Diagnostic {
|
|
return commandkit.Diagnostic{Severity: commandkit.SeverityWarning, Write: func(w io.Writer) error { _, err := io.WriteString(w, message+"\n"); return err }}
|
|
}
|
|
|
|
func TestDetailedValidationFiltersAndRetainsFailureEvidence(t *testing.T) {
|
|
cause := errors.New("second scope failed")
|
|
calls := []string{}
|
|
reader := &sourceInventory{values: []configkit.SourceInfo{{ID: "primary", Label: "main", Path: "main.yml"}, {ID: "related", Label: "included", Path: "include.yml"}, {ID: "unused", Label: "unused"}}}
|
|
records := []configkit.Diagnostic{
|
|
{Code: "chosen", Severity: configkit.SeverityWarning, Path: configkit.Keys("a/b"), Message: "selected warning", Primary: &configkit.Location{SourceID: "primary", Line: 2, Column: 3}, Related: []configkit.Location{{SourceID: "related", Line: 7, Column: 8}}},
|
|
{Code: "other", Severity: configkit.SeverityWarning, Path: configkit.Keys("other"), Message: "other warning"},
|
|
}
|
|
scopes := []Scope[struct{}, struct{}]{
|
|
{Name: "app", Operations: []Operation{Validate}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
|
|
calls = append(calls, "app")
|
|
return commandkit.Acquired[View]{Value: View{Sources: reader, Findings: &Findings{Records: records, Messages: []commandkit.Diagnostic{validationDiagnostic("native warnings")}}}, Diagnostics: []commandkit.Diagnostic{validationDiagnostic("application warning")}}, nil
|
|
}},
|
|
{Name: "second", Operations: []Operation{Validate}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
|
|
calls = append(calls, "second")
|
|
return commandkit.Acquired[View]{Value: View{Findings: &Findings{Records: []configkit.Diagnostic{{Code: "failed", Severity: configkit.SeverityError, Message: "failed finding"}}}}, Explanation: commandkit.Explanation{Message: "terminal failure"}}, cause
|
|
}},
|
|
{Name: "third", Operations: []Operation{Validate}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
|
|
calls = append(calls, "third")
|
|
return commandkit.Acquired[View]{}, nil
|
|
}},
|
|
}
|
|
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: scopes}))
|
|
var out, diag bytes.Buffer
|
|
result := app.Execute(context.Background(), []string{"config", "validate", "--scope=all", "--details", "--severity=warning", "--code=chosen", "--format=json", "--verbose"}, commandkit.Streams{Out: &out, Err: &diag})
|
|
decoder := json.NewDecoder(&out)
|
|
var report validationReport
|
|
if err := decoder.Decode(&report); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := decoder.Decode(new(any)); err != io.EOF {
|
|
t.Fatalf("extra JSON: %v", err)
|
|
}
|
|
if !errors.Is(result.Cause, cause) || result.Kind != commandkit.OutcomeOperational || len(calls) != 2 || calls[0] != "app" || calls[1] != "second" || len(report.Scopes) != 2 || report.Scopes[0].Outcome != "valid" || report.Scopes[1].Outcome != "failed" {
|
|
t.Fatalf("result=%#v calls=%v report=%#v", result, calls, report)
|
|
}
|
|
first, second := report.Scopes[0].Details, report.Scopes[1].Details
|
|
if first == nil || first.State != "present" || len(first.Findings) != 1 || first.Findings[0].Path != "/a~1b" || len(first.Sources) != 2 || first.Sources[0].ID != "primary" || first.Sources[1].ID != "related" || second == nil || second.State != "filtered" || len(second.Findings) != 0 {
|
|
t.Fatalf("details=%#v %#v", first, second)
|
|
}
|
|
if strings.Contains(diag.String(), "native warnings") || !strings.Contains(diag.String(), "application warning") || !strings.Contains(diag.String(), "terminal failure") {
|
|
t.Fatalf("diagnostics=%q", diag.String())
|
|
}
|
|
before := len(calls)
|
|
for _, args := range [][]string{{"--severity=warning"}, {"--code=x"}, {"--details", "--severity=unknown"}} {
|
|
result = app.Execute(context.Background(), append([]string{"config", "validate"}, args...), commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
|
if result.Kind != commandkit.OutcomeUsage || len(calls) != before {
|
|
t.Fatalf("invalid %v: %#v", args, result)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidationCancellationAndDeliveryRetainReachedScopes(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
calls := 0
|
|
scopes := []Scope[struct{}, struct{}]{
|
|
{Name: "app", Operations: []Operation{Validate}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
|
|
calls++
|
|
return commandkit.Acquired[View]{Value: View{Findings: &Findings{}}}, nil
|
|
}},
|
|
{Name: "second", Operations: []Operation{Validate}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
|
|
calls++
|
|
cancel()
|
|
return commandkit.Acquired[View]{}, nil
|
|
}},
|
|
{Name: "third", Operations: []Operation{Validate}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
|
|
calls++
|
|
return commandkit.Acquired[View]{}, nil
|
|
}},
|
|
}
|
|
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: scopes}))
|
|
var out bytes.Buffer
|
|
result := app.Execute(ctx, []string{"config", "validate", "--scope=all", "--details", "--format=json"}, commandkit.Streams{Out: &out, Err: io.Discard})
|
|
var report validationReport
|
|
if err := json.Unmarshal(out.Bytes(), &report); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !errors.Is(result.Cause, context.Canceled) || calls != 2 || len(report.Scopes) != 2 || report.Scopes[0].Details.State != "empty" || report.Scopes[1].Details.State != "unavailable" || report.Scopes[1].Outcome != "canceled" {
|
|
t.Fatalf("result=%#v calls=%d report=%#v", result, calls, report)
|
|
}
|
|
failure := errors.New("load failure")
|
|
scope := Scope[struct{}, struct{}]{Name: "app", Operations: []Operation{Validate}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
|
|
return commandkit.Acquired[View]{}, failure
|
|
}}
|
|
app = newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{scope}}))
|
|
result = app.Execute(context.Background(), []string{"config", "validate", "--details"}, commandkit.Streams{Out: shortWriter{}, Err: io.Discard})
|
|
if !errors.Is(result.Cause, failure) || result.Presentation.Output.State != commandkit.DeliveryIncomplete {
|
|
t.Fatalf("failure and delivery: %#v", result)
|
|
}
|
|
out.Reset()
|
|
result = app.Execute(context.Background(), []string{"config", "validate", "--details", "--quiet"}, commandkit.Streams{Out: &out, Err: io.Discard})
|
|
if !errors.Is(result.Cause, failure) || out.Len() != 0 {
|
|
t.Fatalf("quiet failure: %#v %q", result, out.String())
|
|
}
|
|
}
|
|
|
|
func TestDetailedValidationReportsNativeLoadError(t *testing.T) {
|
|
_, failure := configkit.Load[map[string]any](configkit.Bytes("invalid source", []byte("invalid: [")))
|
|
var native *configkit.LoadError
|
|
if !errors.As(failure, &native) {
|
|
t.Fatalf("fixture: %v", failure)
|
|
}
|
|
scope := FromConfigKit("app", "", func(context.Context, Selection, struct{}, struct{}) (*configkit.Result[map[string]any], error) {
|
|
return nil, failure
|
|
})
|
|
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{scope}}))
|
|
var out bytes.Buffer
|
|
result := app.Execute(context.Background(), []string{"config", "validate", "--details", "--format=json"}, commandkit.Streams{Out: &out, Err: io.Discard})
|
|
var report validationReport
|
|
if err := json.Unmarshal(out.Bytes(), &report); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !errors.Is(result.Cause, failure) || len(report.Scopes) != 1 || report.Scopes[0].Outcome != "failed" || report.Scopes[0].Details.State != "present" || len(report.Scopes[0].Details.Findings) != len(native.Diagnostics()) {
|
|
t.Fatalf("native failure: %#v %q", result, out.String())
|
|
}
|
|
}
|