188 lines
7.3 KiB
Go
188 lines
7.3 KiB
Go
package ckconfig
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
|
|
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
|
)
|
|
|
|
func TestWithScopeErrorCopiesAndRetainsCauses(t *testing.T) {
|
|
first := &scopeCause{message: "first construction problem"}
|
|
second := errors.New("second construction problem")
|
|
original := Scope[struct{}, struct{}]{issues: []error{fmt.Errorf("wrapped: %w", first)}}
|
|
|
|
copyWithError := WithScopeError(original, second)
|
|
if len(original.issues) != 1 || !errors.Is(original.issues[0], first) {
|
|
t.Fatalf("original issues changed: %#v", original.issues)
|
|
}
|
|
if len(copyWithError.issues) != 2 || !errors.Is(copyWithError.issues[0], first) || !errors.Is(copyWithError.issues[1], second) {
|
|
t.Fatalf("attached issues = %#v", copyWithError.issues)
|
|
}
|
|
if copyWithError.issues[0] != original.issues[0] {
|
|
t.Fatal("existing cause identity was not preserved")
|
|
}
|
|
var retained *scopeCause
|
|
if !errors.As(copyWithError.issues[0], &retained) || retained != first {
|
|
t.Fatalf("retained concrete cause = %#v, want %#v", retained, first)
|
|
}
|
|
|
|
narrowed := copyWithError
|
|
narrowed.Operations = nil
|
|
third := errors.New("third construction problem")
|
|
withThird := WithScopeError(narrowed, third)
|
|
unchanged := WithScopeError(withThird, nil)
|
|
if len(unchanged.issues) != len(copyWithError.issues)+1 || !errors.Is(unchanged.issues[2], third) {
|
|
t.Fatalf("repeated attachment lost issues: %#v", unchanged.issues)
|
|
}
|
|
}
|
|
|
|
type scopeCause struct{ message string }
|
|
|
|
func (e *scopeCause) Error() string { return e.message }
|
|
|
|
func TestFromConfigKitRetainsNilLoaderIssue(t *testing.T) {
|
|
scope := FromConfigKit[struct{}, struct{}, struct{}]("app", "Application configuration", nil)
|
|
scope.Operations = nil
|
|
if scope.Acquire != nil {
|
|
t.Fatal("nil loader produced an acquisition callback")
|
|
}
|
|
if len(scope.issues) != 1 || !strings.Contains(scope.issues[0].Error(), "loader") {
|
|
t.Fatalf("retained nil-loader issues = %#v", scope.issues)
|
|
}
|
|
}
|
|
|
|
func TestFromConfigKitPassesInvocationValuesAndNativeFacets(t *testing.T) {
|
|
type input struct{ Label string }
|
|
type state struct{ Calls int }
|
|
type settings struct {
|
|
Name string `yaml:"name"`
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
selection := Selection{
|
|
Config: commandkit.Supplied[string]{Value: "chosen.yaml", Set: true},
|
|
Overlay: commandkit.Supplied[string]{Value: "", Set: true},
|
|
}
|
|
gotContext, gotSelection, gotInput, gotState := context.Context(nil), Selection{}, input{}, (*state)(nil)
|
|
result, err := configkit.Load(configkit.Mapping("source.yaml", map[string]any{"name": "native"}), configkit.Options[settings]{Definition: configkit.Definition[settings]{}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
scope := FromConfigKit[input, *state, settings]("app", "Application configuration", func(got context.Context, selected Selection, in input, st *state) (*configkit.Result[settings], error) {
|
|
gotContext, gotSelection, gotInput, gotState = got, selected, in, st
|
|
st.Calls++
|
|
return result, nil
|
|
})
|
|
if len(scope.Operations) != 4 || scope.Operations[0] != Validate || scope.Operations[1] != Show || scope.Operations[2] != Sources || scope.Operations[3] != Explain {
|
|
t.Fatalf("operations = %#v", scope.Operations)
|
|
}
|
|
if gotContext != nil {
|
|
t.Fatal("loader ran during adapter construction")
|
|
}
|
|
stateValue := &state{}
|
|
acquired, err := scope.Acquire(ctx, Request{Operation: Show, Selection: selection}, input{Label: "typed"}, stateValue)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if gotContext != ctx || gotSelection != selection || gotInput.Label != "typed" || gotState != stateValue || stateValue.Calls != 1 {
|
|
t.Fatalf("loader arguments/context/state = %v, %#v, %#v, %p, %d", gotContext, gotSelection, gotInput, gotState, stateValue.Calls)
|
|
}
|
|
if acquired.Value.Effective != result || acquired.Value.Sources != result || acquired.Value.Explanation != result {
|
|
t.Fatal("adapter did not expose the native result facets directly")
|
|
}
|
|
|
|
var effective bytes.Buffer
|
|
if err := acquired.Value.Effective.WriteEffective(&effective); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(effective.String(), "name: native") {
|
|
t.Fatalf("native effective output = %q", effective.String())
|
|
}
|
|
sources := acquired.Value.Sources.Sources()
|
|
if len(sources) != 1 || sources[0].Label != "source.yaml" {
|
|
t.Fatalf("native sources = %#v", sources)
|
|
}
|
|
path, err := configkit.ParsePath("/name")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := acquired.Value.Explanation.Explain(path); !ok {
|
|
t.Fatal("native explanation facet did not resolve /name")
|
|
}
|
|
var explanation bytes.Buffer
|
|
if err := acquired.Value.Explanation.WriteExplanation(&explanation, path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if explanation.Len() == 0 {
|
|
t.Fatal("native explanation writer produced no output")
|
|
}
|
|
}
|
|
|
|
func TestFromConfigKitRetainsLoadErrorAndDiagnostics(t *testing.T) {
|
|
type settings struct {
|
|
Name string `yaml:"name"`
|
|
}
|
|
_, loadErr := configkit.Load(configkit.Mapping("bad source", map[string]any{"unknown": true}), configkit.Options[settings]{Definition: configkit.Definition[settings]{}})
|
|
if loadErr == nil {
|
|
t.Fatal("fixture did not produce a ConfigKit load error")
|
|
}
|
|
scope := FromConfigKit[struct{}, struct{}, settings]("app", "", func(context.Context, Selection, struct{}, struct{}) (*configkit.Result[settings], error) {
|
|
return nil, loadErr
|
|
})
|
|
acquired, gotErr := scope.Acquire(context.Background(), Request{}, struct{}{}, struct{}{})
|
|
if gotErr != loadErr {
|
|
t.Fatalf("load error identity = %v, want original %v", gotErr, loadErr)
|
|
}
|
|
var typed *configkit.LoadError
|
|
if !errors.As(gotErr, &typed) {
|
|
t.Fatalf("load error type was not retained: %T", gotErr)
|
|
}
|
|
if len(acquired.Diagnostics) != 1 || acquired.Diagnostics[0].Severity != commandkit.SeverityError {
|
|
t.Fatalf("failure diagnostics = %#v", acquired.Diagnostics)
|
|
}
|
|
var rendered bytes.Buffer
|
|
if err := acquired.Diagnostics[0].Write(&rendered); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(rendered.String(), `Configuration scope "app":`) || !strings.Contains(rendered.String(), `"bad source"`) {
|
|
t.Fatalf("diagnostic output lacks scope/source legend: %q", rendered.String())
|
|
}
|
|
}
|
|
|
|
func TestConfigKitDiagnosticsMapSeverityAndPropagateWriterFailures(t *testing.T) {
|
|
warning := configKitDiagnostics("app", nil, []configkit.Diagnostic{{Severity: configkit.SeverityWarning, Message: "safe warning"}})
|
|
if len(warning) != 1 || warning[0].Severity != commandkit.SeverityWarning {
|
|
t.Fatalf("warning diagnostics = %#v", warning)
|
|
}
|
|
withError := configKitDiagnostics("app", nil, []configkit.Diagnostic{
|
|
{Severity: configkit.SeverityWarning, Message: "safe warning"},
|
|
{Severity: configkit.SeverityError, Message: "safe error"},
|
|
})
|
|
if withError[0].Severity != commandkit.SeverityError {
|
|
t.Fatalf("mixed diagnostics severity = %q", withError[0].Severity)
|
|
}
|
|
|
|
if err := withError[0].Write(shortWriter{}); !errors.Is(err, io.ErrShortWrite) {
|
|
t.Fatalf("short writer error = %v, want %v", err, io.ErrShortWrite)
|
|
}
|
|
wantErr := errors.New("writer failed")
|
|
if err := withError[0].Write(failingWriter{err: wantErr}); !errors.Is(err, wantErr) {
|
|
t.Fatalf("writer error = %v, want %v", err, wantErr)
|
|
}
|
|
}
|
|
|
|
type shortWriter struct{}
|
|
|
|
func (shortWriter) Write(value []byte) (int, error) { return len(value) - 1, nil }
|
|
|
|
type failingWriter struct{ err error }
|
|
|
|
func (w failingWriter) Write([]byte) (int, error) { return 0, w.err }
|