Files
commandkit/integrations/ckconfig/contracts_adapter_test.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 }