Finish maintained consumer examples
This commit is contained in:
@@ -1,78 +0,0 @@
|
||||
package ckconfig_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStandardConfigurationCancellationBoundaries(t *testing.T) {
|
||||
for _, operation := range []string{"validate", "show", "sources", "explain"} {
|
||||
for _, before := range []bool{true, false} {
|
||||
t.Run(fmt.Sprintf("%s/before=%t", operation, before), func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
if before {
|
||||
cancel()
|
||||
}
|
||||
loads, contexts := 0, 0
|
||||
scope, err := commandkit.NewConfigKitScope[struct{}, struct{}]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[struct{}], error) {
|
||||
loads++
|
||||
cancel()
|
||||
return configkit.Load(configkit.NoSource(), configkit.Options[struct{}]{})
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{scope}, NewContext: func() struct{} { contexts++; return struct{}{} }}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
args := []string{"config", operation}
|
||||
if operation == "explain" {
|
||||
args = append(args, "--path", "")
|
||||
}
|
||||
var out bytes.Buffer
|
||||
result := app.Execute(ctx, args, commandkit.Streams{Out: &out})
|
||||
want := 1
|
||||
if before {
|
||||
want = 0
|
||||
}
|
||||
if result.Kind != commandkit.OutcomeOperational || result.Status != 1 || !errors.Is(result.Cause, context.Canceled) || loads != want || contexts != want || out.Len() != 0 {
|
||||
t.Fatalf("result=%#v loads=%d contexts=%d out=%q", result, loads, contexts, out.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAggregateCancellationRetainsCompletedValidationAndDiagnostics(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
calls := 0
|
||||
acquire := func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||
calls++
|
||||
if calls == 2 {
|
||||
cancel()
|
||||
}
|
||||
return commandkit.Acquired[commandkit.ConfigView]{Diagnostics: []commandkit.Diagnostic{commandkit.MessageDiagnostic(configkit.SeverityWarning, "acquired warning")}}, nil
|
||||
}
|
||||
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{
|
||||
{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: acquire},
|
||||
{Name: "promptkit", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: acquire},
|
||||
{Name: "zeta", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: acquire},
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var out, diagnostics bytes.Buffer
|
||||
result := app.Execute(ctx, []string{"config", "validate", "--scope", "all"}, commandkit.Streams{Out: &out, Err: &diagnostics})
|
||||
if !errors.Is(result.Cause, context.Canceled) || !result.ReportIncomplete || calls != 2 || out.String() != "app: valid\n" || strings.Count(diagnostics.String(), "acquired warning") != 2 {
|
||||
t.Fatalf("result=%#v calls=%d out=%q diagnostics=%q", result, calls, out.String(), diagnostics.String())
|
||||
}
|
||||
}
|
||||
@@ -1,359 +0,0 @@
|
||||
package ckconfig
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigScopeDiscoveryOrdersAndDeduplicatesCapabilities(t *testing.T) {
|
||||
app, err := New(Identity{Name: "example"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
newContextCalls := 0
|
||||
acquireCalls := 0
|
||||
suite := ConfigSuite[struct{}]{
|
||||
NewContext: func() struct{} { newContextCalls++; return struct{}{} },
|
||||
Scopes: []ConfigScope[struct{}]{
|
||||
{Name: "zeta", Operations: []ConfigOperation{ConfigExplain, ConfigShow, ConfigExplain}, Acquire: func(context.Context, ConfigRequest, struct{}) (Acquired[ConfigView], error) {
|
||||
acquireCalls++
|
||||
return Acquired[ConfigView]{}, nil
|
||||
}},
|
||||
{Name: "promptkit", Operations: []ConfigOperation{ConfigSources}, Acquire: noopConfigAcquire[struct{}]},
|
||||
{Name: "app", Operations: []ConfigOperation{ConfigShow, ConfigValidate, ConfigShow}, Acquire: func(context.Context, ConfigRequest, struct{}) (Acquired[ConfigView], error) {
|
||||
acquireCalls++
|
||||
return Acquired[ConfigView]{}, nil
|
||||
}},
|
||||
{Name: "metadata", Summary: "No provider is required."},
|
||||
},
|
||||
}
|
||||
normalized, err := normalizeConfigSuite(suite)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(normalized.Scopes[0].Operations, []ConfigOperation{ConfigShow, ConfigExplain}) || !reflect.DeepEqual(normalized.Scopes[2].Operations, []ConfigOperation{ConfigValidate, ConfigShow}) {
|
||||
t.Fatalf("normalized capabilities = %#v", normalized.Scopes)
|
||||
}
|
||||
if err := RegisterConfig(app, suite); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var textOutput bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), []string{"config", "scopes"}, Streams{Out: &textOutput})
|
||||
if outcome.Kind != OutcomeSuccess {
|
||||
t.Fatalf("text inventory outcome = %#v", outcome)
|
||||
}
|
||||
wantText := "app: validate, show\npromptkit: sources\nmetadata:\nzeta: show, explain\n"
|
||||
if textOutput.String() != wantText {
|
||||
t.Fatalf("text inventory = %q, want %q", textOutput.String(), wantText)
|
||||
}
|
||||
|
||||
var yamlOutput bytes.Buffer
|
||||
outcome = app.Execute(context.Background(), []string{"config", "scopes", "--format", "yaml"}, Streams{Out: &yamlOutput})
|
||||
if outcome.Kind != OutcomeSuccess {
|
||||
t.Fatalf("YAML inventory outcome = %#v", outcome)
|
||||
}
|
||||
if got := yamlOutput.String(); !strings.Contains(got, "scopes:") || strings.Index(got, "name: app") > strings.Index(got, "name: promptkit") || !strings.Contains(got, "operations: []") {
|
||||
t.Fatalf("YAML inventory = %q", got)
|
||||
}
|
||||
outcome = app.Execute(context.Background(), []string{"config", "scopes", "--help"}, Streams{})
|
||||
if outcome.Kind != OutcomeSuccess {
|
||||
t.Fatalf("scope help outcome = %#v", outcome)
|
||||
}
|
||||
outcome = app.Execute(context.Background(), []string{"config", "scopes", "--format", "json"}, Streams{})
|
||||
if outcome.Kind != OutcomeUsage {
|
||||
t.Fatalf("invalid format outcome = %#v", outcome)
|
||||
}
|
||||
if newContextCalls != 0 || acquireCalls != 0 {
|
||||
t.Fatalf("discovery called runtime callbacks: NewContext=%d Acquire=%d", newContextCalls, acquireCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigInputDefaultsToAppAndRetainsAbsentSelections(t *testing.T) {
|
||||
suite := ConfigSuite[struct{}]{Scopes: []ConfigScope[struct{}]{{Name: "app"}}}
|
||||
prepared, err := prepareInput(CommandSpec[configInput[struct{}]]{Flags: configInputFlags(suite, "yaml")}, make([][]string, 4), nil, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
input := prepared.build()
|
||||
if input.Scope != "app" || input.Format != "yaml" || input.Selection.Config.Set || input.Selection.Overlay.Set {
|
||||
t.Fatalf("configuration defaults = %#v", input)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterConfigOwnsPreparedScopeAndSelectorMetadata(t *testing.T) {
|
||||
type selectorInput struct{ Region Supplied[string] }
|
||||
var acquired selectorInput
|
||||
suite := ConfigSuite[selectorInput]{
|
||||
Selectors: []Selector[selectorInput]{{
|
||||
Binding: ValueFlag[selectorInput](FlagMeta{Name: "region"}, nil, parseString, func(input *selectorInput, value Supplied[string]) { input.Region = value }),
|
||||
Scopes: []string{"app"},
|
||||
}},
|
||||
Scopes: []ConfigScope[selectorInput]{{
|
||||
Name: "app",
|
||||
Operations: []ConfigOperation{ConfigValidate},
|
||||
Acquire: func(_ context.Context, _ ConfigRequest, input selectorInput) (Acquired[ConfigView], error) {
|
||||
acquired = input
|
||||
return Acquired[ConfigView]{}, nil
|
||||
},
|
||||
}},
|
||||
}
|
||||
app, err := New(Identity{Name: "example"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := RegisterConfig(app, suite); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
suite.Scopes[0].Name = "renamed"
|
||||
suite.Scopes[0].Operations[0] = ConfigShow
|
||||
suite.Selectors[0].Scopes[0] = "renamed"
|
||||
|
||||
var inventory bytes.Buffer
|
||||
if result := app.Execute(context.Background(), []string{"config", "scopes"}, Streams{Out: &inventory}); result.Kind != OutcomeSuccess {
|
||||
t.Fatalf("scope inventory outcome = %#v", result)
|
||||
}
|
||||
if got := inventory.String(); got != "app: validate\n" {
|
||||
t.Fatalf("scope inventory after caller mutation = %q", got)
|
||||
}
|
||||
if result := app.Execute(context.Background(), []string{"config", "validate", "--scope=app", "--region=west"}, Streams{}); result.Kind != OutcomeSuccess {
|
||||
t.Fatalf("registered scope execution after caller mutation = %#v", result)
|
||||
}
|
||||
if acquired.Region != (Supplied[string]{Value: "west", Set: true}) {
|
||||
t.Fatalf("acquired selector after caller mutation = %#v", acquired)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterConfigRejectsDuplicateSelectorNamesAndShorts(t *testing.T) {
|
||||
type selectorInput struct{ First, Second string }
|
||||
tests := []struct {
|
||||
name string
|
||||
firstMeta FlagMeta
|
||||
secondMeta FlagMeta
|
||||
}{
|
||||
{name: "name", firstMeta: FlagMeta{Name: "region", Short: "r"}, secondMeta: FlagMeta{Name: "region", Short: "g"}},
|
||||
{name: "short", firstMeta: FlagMeta{Name: "region", Short: "r"}, secondMeta: FlagMeta{Name: "group", Short: "r"}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
app, err := New(Identity{Name: "example"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
suite := ConfigSuite[selectorInput]{Selectors: []Selector[selectorInput]{
|
||||
{Binding: ValueFlag[selectorInput](test.firstMeta, nil, parseString, func(input *selectorInput, value Supplied[string]) { input.First = value.Value })},
|
||||
{Binding: ValueFlag[selectorInput](test.secondMeta, nil, parseString, func(input *selectorInput, value Supplied[string]) { input.Second = value.Value })},
|
||||
}}
|
||||
if err := RegisterConfig(app, suite); err == nil || !strings.Contains(err.Error(), "duplicate configuration selector flag") {
|
||||
t.Fatalf("RegisterConfig error = %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterConfigRejectsInvalidSuitesAndCollisions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
scope ConfigScope[struct{}]
|
||||
want string
|
||||
}{
|
||||
{name: "empty name", scope: ConfigScope[struct{}]{}, want: "name must not be empty"},
|
||||
{name: "all name", scope: ConfigScope[struct{}]{Name: "all"}, want: "reserved"},
|
||||
{name: "unknown operation", scope: ConfigScope[struct{}]{Name: "app", Operations: []ConfigOperation{"diff"}}, want: "unknown operation"},
|
||||
{name: "missing acquisition", scope: ConfigScope[struct{}]{Name: "app", Operations: []ConfigOperation{ConfigValidate}}, want: "without an acquisition"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
app, _ := New(Identity{Name: "example"})
|
||||
err := RegisterConfig(app, ConfigSuite[struct{}]{Scopes: []ConfigScope[struct{}]{test.scope}})
|
||||
if err == nil || !strings.Contains(err.Error(), test.want) {
|
||||
t.Fatalf("RegisterConfig error = %v, want %q", err, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
app, _ := New(Identity{Name: "example"})
|
||||
custom, err := NewCommand(CommandSpec[struct{}]{Name: "config", Run: func(Invocation, struct{}) Outcome { return Success() }})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := app.Register(custom); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := RegisterConfig(app, ConfigSuite[struct{}]{}); err == nil {
|
||||
t.Fatal("replaced a pre-existing config command")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigSelectorPreflightAndScopedContextProjection(t *testing.T) {
|
||||
type selectorInput struct {
|
||||
Count Supplied[int]
|
||||
Enabled Supplied[bool]
|
||||
Other Supplied[int]
|
||||
Runtime string
|
||||
}
|
||||
baseContextCalls := 0
|
||||
checkCalls := 0
|
||||
var checked selectorInput
|
||||
countDefault := 9
|
||||
suite := ConfigSuite[selectorInput]{
|
||||
NewContext: func() selectorInput { baseContextCalls++; return selectorInput{Runtime: "invocation cache"} },
|
||||
Selectors: []Selector[selectorInput]{
|
||||
{Binding: ValueFlag[selectorInput](FlagMeta{Name: "count"}, &countDefault, strconv.Atoi, func(input *selectorInput, value Supplied[int]) { input.Count = value }), Scopes: []string{"app"}},
|
||||
{Binding: BoolFlag[selectorInput](FlagMeta{Name: "enabled"}, true, func(input *selectorInput, value Supplied[bool]) { input.Enabled = value }), Scopes: []string{"promptkit"}},
|
||||
{Binding: ValueFlag[selectorInput](FlagMeta{Name: "other"}, nil, strconv.Atoi, func(input *selectorInput, value Supplied[int]) { input.Other = value })},
|
||||
},
|
||||
CheckInput: func(_ Selection, input selectorInput) error { checkCalls++; checked = input; return nil },
|
||||
Scopes: []ConfigScope[selectorInput]{
|
||||
{Name: "app", Operations: []ConfigOperation{ConfigValidate}, Acquire: noopConfigAcquire[selectorInput]},
|
||||
{Name: "promptkit", Operations: []ConfigOperation{ConfigValidate}, Acquire: noopConfigAcquire[selectorInput]},
|
||||
},
|
||||
}
|
||||
normalized, err := normalizeConfigSuite(suite)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
preparedSuite := prepareConfigSuite(normalized)
|
||||
spec := CommandSpec[configInput[selectorInput]]{Flags: configInputFlags(normalized, "text")}
|
||||
prepared, err := prepareInput(spec, [][]string{{"all"}, {"text"}, {""}, nil, {"0"}, {"false"}, nil}, nil, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
input := prepared.build()
|
||||
scopeNames, err := preflightConfig(preparedSuite, input, ConfigValidate)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(scopeNames, []string{"app", "promptkit"}) || checkCalls != 1 || checked.Count != (Supplied[int]{Value: 0, Set: true}) || checked.Enabled != (Supplied[bool]{Value: false, Set: true}) {
|
||||
t.Fatalf("preflight result=%v checked=%#v calls=%d", scopeNames, checked, checkCalls)
|
||||
}
|
||||
if !input.Selection.Config.Set || input.Selection.Config.Value != "" || input.Selection.Overlay.Set {
|
||||
t.Fatalf("configuration selection presence = %#v", input.Selection)
|
||||
}
|
||||
if baseContextCalls != 0 {
|
||||
t.Fatalf("NewContext called during preflight %d times", baseContextCalls)
|
||||
}
|
||||
|
||||
contexts := contextsForScopes(preparedSuite, input, scopeNames)
|
||||
if baseContextCalls != 1 {
|
||||
t.Fatalf("NewContext calls = %d, want one", baseContextCalls)
|
||||
}
|
||||
if contexts["app"].Count != (Supplied[int]{Value: 0, Set: true}) || contexts["app"].Enabled != (Supplied[bool]{}) || contexts["app"].Other != (Supplied[int]{}) {
|
||||
t.Fatalf("app selectors = %#v", contexts["app"])
|
||||
}
|
||||
if contexts["promptkit"].Count != (Supplied[int]{}) || contexts["promptkit"].Enabled != (Supplied[bool]{Value: false, Set: true}) || contexts["promptkit"].Runtime != "invocation cache" {
|
||||
t.Fatalf("promptkit selectors = %#v", contexts["promptkit"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigPreflightRejectsUnsupportedAggregateAndInapplicableSelector(t *testing.T) {
|
||||
type selectorInput struct{ Region Supplied[string] }
|
||||
newContextCalls := 0
|
||||
suite := ConfigSuite[selectorInput]{
|
||||
NewContext: func() selectorInput { newContextCalls++; return selectorInput{} },
|
||||
Selectors: []Selector[selectorInput]{{Binding: ValueFlag[selectorInput](FlagMeta{Name: "region"}, nil, parseString, func(input *selectorInput, value Supplied[string]) { input.Region = value }), Scopes: []string{"promptkit"}}},
|
||||
Scopes: []ConfigScope[selectorInput]{
|
||||
{Name: "app", Operations: []ConfigOperation{ConfigValidate}, Acquire: noopConfigAcquire[selectorInput]},
|
||||
{Name: "promptkit", Operations: []ConfigOperation{ConfigShow}, Acquire: noopConfigAcquire[selectorInput]},
|
||||
},
|
||||
}
|
||||
normalized, err := normalizeConfigSuite(suite)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
preparedSuite := prepareConfigSuite(normalized)
|
||||
prepared, err := prepareInput(CommandSpec[configInput[selectorInput]]{Flags: configInputFlags(normalized, "text")}, [][]string{{"all"}, {"text"}, nil, nil, nil}, nil, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := preflightConfig(preparedSuite, prepared.build(), ConfigValidate); err == nil || !strings.Contains(err.Error(), "does not support validate") {
|
||||
t.Fatalf("unsupported aggregate error = %v", err)
|
||||
}
|
||||
if newContextCalls != 0 {
|
||||
t.Fatalf("unsupported aggregate created context %d times", newContextCalls)
|
||||
}
|
||||
|
||||
prepared, err = prepareInput(CommandSpec[configInput[selectorInput]]{Flags: configInputFlags(normalized, "text")}, [][]string{{"app"}, {"text"}, nil, nil, {"west"}}, nil, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := preflightConfig(preparedSuite, prepared.build(), ConfigValidate); err == nil || !strings.Contains(err.Error(), "not applicable") {
|
||||
t.Fatalf("inapplicable selector error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func noopConfigAcquire[C any](context.Context, ConfigRequest, C) (Acquired[ConfigView], error) {
|
||||
return Acquired[ConfigView]{}, nil
|
||||
}
|
||||
|
||||
func TestRequiredConfigSelectorOnlyAppliesToSelectedScopes(t *testing.T) {
|
||||
app, _ := New(Identity{Name: "example"})
|
||||
calls := 0
|
||||
acquire := func(context.Context, ConfigRequest, string) (Acquired[ConfigView], error) {
|
||||
calls++
|
||||
return Acquired[ConfigView]{}, nil
|
||||
}
|
||||
err := RegisterConfig(app, ConfigSuite[string]{
|
||||
Selectors: []Selector[string]{{Scopes: []string{"app"}, Binding: ValueFlag[string](FlagMeta{Name: "campaign", Required: true}, nil, parseString, func(c *string, v Supplied[string]) { *c = v.Value })}},
|
||||
Scopes: []ConfigScope[string]{{Name: "app", Operations: []ConfigOperation{ConfigValidate}, Acquire: acquire}, {Name: "promptkit", Operations: []ConfigOperation{ConfigValidate}, Acquire: acquire}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
args []string
|
||||
kind OutcomeKind
|
||||
calls int
|
||||
}{
|
||||
{[]string{"--scope", "promptkit"}, OutcomeSuccess, 1},
|
||||
{nil, OutcomeUsage, 0},
|
||||
{[]string{"--scope", "all"}, OutcomeUsage, 0},
|
||||
{[]string{"--scope", "all", "--campaign", "spring"}, OutcomeSuccess, 2},
|
||||
{[]string{"--scope", "promptkit", "--campaign", "spring"}, OutcomeUsage, 0},
|
||||
{[]string{"--help"}, OutcomeSuccess, 0},
|
||||
} {
|
||||
calls = 0
|
||||
var out bytes.Buffer
|
||||
result := app.Execute(context.Background(), append([]string{"config", "validate"}, tc.args...), Streams{Out: &out})
|
||||
if result.Kind != tc.kind || calls != tc.calls {
|
||||
t.Fatalf("%v: result=%#v calls=%d", tc.args, result, calls)
|
||||
}
|
||||
if len(tc.args) > 0 && tc.args[0] == "--help" && !strings.Contains(out.String(), "required where applicable") {
|
||||
t.Fatalf("help=%q", out.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnknownConfigScopeReportsChoicesWithoutRuntimeWork(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
names []string
|
||||
scope []string
|
||||
choices string
|
||||
}{
|
||||
{[]string{"promptkit"}, nil, "available: promptkit"},
|
||||
{[]string{"zeta", "alpha", "promptkit", "app"}, []string{"--scope", "unknown"}, "available: app, promptkit, alpha, zeta"},
|
||||
} {
|
||||
runtimeCalls := 0
|
||||
suite := ConfigSuite[struct{}]{NewContext: func() struct{} { runtimeCalls++; return struct{}{} }}
|
||||
for _, name := range tc.names {
|
||||
suite.Scopes = append(suite.Scopes, ConfigScope[struct{}]{Name: name, Operations: []ConfigOperation{ConfigValidate}, Acquire: func(context.Context, ConfigRequest, struct{}) (Acquired[ConfigView], error) {
|
||||
runtimeCalls++
|
||||
return Acquired[ConfigView]{}, nil
|
||||
}})
|
||||
}
|
||||
app, _ := New(Identity{Name: "example"})
|
||||
if err := RegisterConfig(app, suite); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var out, diagnostics bytes.Buffer
|
||||
result := app.Execute(context.Background(), append([]string{"config", "validate"}, tc.scope...), Streams{Out: &out, Err: &diagnostics})
|
||||
if result.Kind != OutcomeUsage || runtimeCalls != 0 || out.Len() != 0 || !strings.Contains(diagnostics.String(), tc.choices) {
|
||||
t.Fatalf("result=%#v calls=%d out=%q diagnostics=%q", result, runtimeCalls, out.String(), diagnostics.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,720 +0,0 @@
|
||||
package ckconfig_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
type effectiveYAML string
|
||||
|
||||
func (document effectiveYAML) WriteEffective(writer io.Writer) error {
|
||||
_, err := io.WriteString(writer, string(document))
|
||||
return err
|
||||
}
|
||||
|
||||
type capturedSourceReader struct {
|
||||
entries []configkit.SourceInfo
|
||||
calls int
|
||||
}
|
||||
|
||||
func TestConfigExplainPreservesConfigKitProvenanceAndRedaction(t *testing.T) {
|
||||
type entry struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Credential configkit.Secret `yaml:"credential"`
|
||||
}
|
||||
type configuration struct {
|
||||
Entries map[string]entry `yaml:"entries"`
|
||||
Odd map[string]string `yaml:"odd"`
|
||||
Value string `yaml:"value"`
|
||||
Dependency string `yaml:"dependency"`
|
||||
Secret configkit.Secret `yaml:"secret"`
|
||||
}
|
||||
const secretValue = "EXPLAIN_TEST_SECRET_VALUE"
|
||||
t.Setenv("EXPLAIN_TEST_SECRET", secretValue)
|
||||
input := []byte("entries:\n parent:\n enabled: false\n credential: EXPLAIN_TEST_SECRET\n child:\n extends: parent\nodd:\n \"key/name~\": escaped\nvalue: original\ndependency: source\nsecret: EXPLAIN_TEST_SECRET\n")
|
||||
result, err := configkit.Load[configuration](configkit.Bytes("explanation.yml", input), configkit.Options[configuration]{Definition: configkit.Definition[configuration]{
|
||||
Secret: func(path configkit.Path) bool { return path.String() == "/entries/parent/credential" },
|
||||
Resolve: func(value *configuration, resolution *configkit.Resolution) error {
|
||||
resolution.Record(configkit.Keys("value"), configkit.ChangeNormalization, "upper case", configkit.Keys("dependency"))
|
||||
value.Value = strings.ToUpper(value.Value)
|
||||
return nil
|
||||
},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loads, contexts := 0, 0
|
||||
scope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[configuration], error) {
|
||||
loads++
|
||||
return result, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
app, err := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{NewContext: func() struct{} { contexts++; return struct{}{} }, Scopes: []commandkit.ConfigScope[struct{}]{scope}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runYAML := func(pointer string) (map[string]any, commandkit.Outcome, string) {
|
||||
var output bytes.Buffer
|
||||
argv := []string{"config", "explain", "--path", pointer, "--format", "yaml"}
|
||||
if pointer == "" {
|
||||
argv = []string{"config", "explain", "--path=", "--format", "yaml"}
|
||||
}
|
||||
outcome := app.Execute(context.Background(), argv, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||
var report map[string]any
|
||||
if outcome.Kind == commandkit.OutcomeSuccess {
|
||||
if err := yaml.Unmarshal(output.Bytes(), &report); err != nil {
|
||||
t.Fatalf("decode explanation YAML: %v; output=%q", err, output.String())
|
||||
}
|
||||
}
|
||||
return report, outcome, output.String()
|
||||
}
|
||||
|
||||
path := "/entries/child/enabled"
|
||||
var native bytes.Buffer
|
||||
if err := result.WriteExplanation(&native, configkit.Keys("entries", "child", "enabled")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var textOutput bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), []string{"config", "explain", "--path", path}, commandkit.Streams{Out: &textOutput, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || textOutput.String() != native.String() {
|
||||
t.Fatalf("native text outcome=%#v output=%q want=%q", outcome, textOutput.String(), native.String())
|
||||
}
|
||||
var inherited map[string]any
|
||||
inherited, outcome, _ = runYAML(path)
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || inherited["path"] != path {
|
||||
t.Fatalf("inherited explanation outcome=%#v report=%#v", outcome, inherited)
|
||||
}
|
||||
effective := inherited["effective"].(map[string]any)
|
||||
if effective["present"] != true || effective["yaml"] != "false\n" {
|
||||
t.Fatalf("inherited effective snapshot = %#v", effective)
|
||||
}
|
||||
origin := inherited["origin"].(map[string]any)
|
||||
steps := origin["inheritance"].([]any)
|
||||
step := steps[0].(map[string]any)
|
||||
if step["from"] != "/entries/parent/enabled" || step["to"] != path || step["reference"] != "/entries/child/extends" {
|
||||
t.Fatalf("inheritance paths = %#v", step)
|
||||
}
|
||||
|
||||
escaped, outcome, _ := runYAML("/odd/key~1name~0")
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || escaped["path"] != "/odd/key~1name~0" || escaped["effective"].(map[string]any)["yaml"] != "escaped\n" {
|
||||
t.Fatalf("escaped path outcome=%#v report=%#v", outcome, escaped)
|
||||
}
|
||||
historical, outcome, _ := runYAML("/entries/child/extends")
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || historical["effective"].(map[string]any)["present"] != false || len(historical["history"].([]any)) == 0 {
|
||||
t.Fatalf("historical path outcome=%#v report=%#v", outcome, historical)
|
||||
}
|
||||
resolved, outcome, _ := runYAML("/value")
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || resolved["effective"].(map[string]any)["yaml"] != "ORIGINAL\n" {
|
||||
t.Fatalf("resolved snapshot outcome=%#v report=%#v", outcome, resolved)
|
||||
}
|
||||
changes := resolved["history"].([]any)
|
||||
last := changes[len(changes)-1].(map[string]any)
|
||||
if last["reasons"].([]any)[0] != "upper case" || last["dependencies"].([]any)[0] != "/dependency" {
|
||||
t.Fatalf("resolution change = %#v", last)
|
||||
}
|
||||
secretReport, outcome, secretText := runYAML("/entries/child/credential")
|
||||
secretEffective := secretReport["effective"].(map[string]any)
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || secretEffective["redacted"] != true || strings.Contains(secretText, secretValue) {
|
||||
t.Fatalf("secret report outcome=%#v report=%#v", outcome, secretReport)
|
||||
}
|
||||
for _, change := range secretReport["history"].([]any) {
|
||||
entry := change.(map[string]any)
|
||||
for _, key := range []string{"before", "after"} {
|
||||
snapshot := entry[key].(map[string]any)
|
||||
if snapshot["present"] == true && snapshot["redacted"] != true {
|
||||
t.Fatalf("secret history was not redacted: %#v", entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
directSecret, outcome, directSecretText := runYAML("/secret")
|
||||
secretRef, refPresent := directSecret["secret_reference"].(map[string]any)
|
||||
if !refPresent {
|
||||
t.Fatalf("secret reference was absent: %#v", directSecret)
|
||||
}
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || secretRef["name"] != "EXPLAIN_TEST_SECRET" || strings.Contains(directSecretText, secretValue) {
|
||||
t.Fatalf("direct secret reference outcome=%#v report=%#v", outcome, directSecret)
|
||||
}
|
||||
root, outcome, _ := runYAML("")
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || root["path"] != "" {
|
||||
t.Fatalf("root path outcome=%#v report=%#v", outcome, root)
|
||||
}
|
||||
|
||||
loadsBeforeInvalid, contextsBeforeInvalid := loads, contexts
|
||||
outcome = app.Execute(context.Background(), []string{"config", "explain"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeUsage || loads != loadsBeforeInvalid || contexts != contextsBeforeInvalid {
|
||||
t.Fatalf("missing path outcome=%#v loads=%d contexts=%d", outcome, loads, contexts)
|
||||
}
|
||||
outcome = app.Execute(context.Background(), []string{"config", "explain", "--path", "/bad~2path"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeUsage || outcome.Status != 2 || loads != loadsBeforeInvalid || contexts != contextsBeforeInvalid {
|
||||
t.Fatalf("malformed path outcome=%#v loads=%d contexts=%d", outcome, loads, contexts)
|
||||
}
|
||||
outcome = app.Execute(context.Background(), []string{"config", "explain", "--path", "/does-not-exist"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeOperational || outcome.Status != 1 {
|
||||
t.Fatalf("unknown path outcome = %#v", outcome)
|
||||
}
|
||||
partial := &configKitShowPartialWriter{failure: io.ErrClosedPipe}
|
||||
outcome = app.Execute(context.Background(), []string{"config", "explain", "--path", path}, commandkit.Streams{Out: partial, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeOperational || !outcome.ReportIncomplete || !errors.Is(outcome.DeliveryError, io.ErrClosedPipe) {
|
||||
t.Fatalf("explanation writer failure = %#v", outcome)
|
||||
}
|
||||
}
|
||||
|
||||
func (reader *capturedSourceReader) Sources() []configkit.SourceInfo {
|
||||
reader.calls++
|
||||
return reader.entries
|
||||
}
|
||||
|
||||
func TestConfigSourcesReportsCapturedConfigKitSourceInventory(t *testing.T) {
|
||||
type configuration struct {
|
||||
Value string `yaml:"value"`
|
||||
Shared string `yaml:"shared"`
|
||||
Synthetic string `yaml:"synthetic"`
|
||||
}
|
||||
filesystem := configkit.NewFilesystem("fixture", fstest.MapFS{
|
||||
"root/config/main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: ../shared.yml}]}\nvalue: base\n")},
|
||||
"root/shared.yml": &fstest.MapFile{Data: []byte("shared: imported\n")},
|
||||
"root/overlays/dev.yml": &fstest.MapFile{Data: []byte("value: overlay\n")},
|
||||
})
|
||||
filesystemContext := &configkit.FilesystemContext{Root: "root", FS: &filesystem}
|
||||
overlayFilesystemContext := &configkit.FilesystemContext{Root: "root", BaseDir: "root/config", FS: &filesystem}
|
||||
loaderCalls := 0
|
||||
reader := &capturedSourceReader{}
|
||||
scope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[configuration], error) {
|
||||
loaderCalls++
|
||||
result, err := configkit.LoadLayered(configkit.Input{
|
||||
Source: filesystem.File("root/config/main.yml"),
|
||||
Imports: true,
|
||||
Filesystem: filesystemContext,
|
||||
}, configkit.LayeredOptions[configuration]{
|
||||
Definition: configkit.Definition[configuration]{},
|
||||
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Mapping("synthetic source", map[string]any{"synthetic": "underlay"})}}}},
|
||||
Overlay: "dev",
|
||||
OverlayFilesystem: overlayFilesystemContext,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reader.entries = result.Sources()
|
||||
return result, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Replace the result's reader after the owning loader has captured its source inventory.
|
||||
originalAcquire := scope.Acquire
|
||||
scope.Acquire = func(ctx context.Context, request commandkit.ConfigRequest, input struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||
acquired, err := originalAcquire(ctx, request, input)
|
||||
if err == nil {
|
||||
acquired.Value.Sources = reader
|
||||
}
|
||||
return acquired, err
|
||||
}
|
||||
// Register the wrapped scope so the command reads the captured public facet once.
|
||||
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{scope}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var output bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), []string{"config", "sources", "--format", "yaml"}, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || loaderCalls != 1 || reader.calls != 1 {
|
||||
var details bytes.Buffer
|
||||
for _, diagnostic := range outcome.Diagnostics {
|
||||
_ = diagnostic.Write(&details)
|
||||
}
|
||||
t.Fatalf("sources outcome=%#v cause=%v details=%q loader calls=%d reader calls=%d", outcome, outcome.Cause, details.String(), loaderCalls, reader.calls)
|
||||
}
|
||||
var report struct {
|
||||
Scope string `yaml:"scope"`
|
||||
Sources []struct {
|
||||
ID string `yaml:"id"`
|
||||
Label string `yaml:"label"`
|
||||
Path string `yaml:"path"`
|
||||
Layer string `yaml:"layer"`
|
||||
Kind string `yaml:"kind"`
|
||||
UnderlayIndex int `yaml:"underlay_index"`
|
||||
ImportedFrom *struct {
|
||||
SourceID string `yaml:"source_id"`
|
||||
Line int `yaml:"line"`
|
||||
Column int `yaml:"column"`
|
||||
} `yaml:"imported_from"`
|
||||
} `yaml:"sources"`
|
||||
}
|
||||
if err := yaml.Unmarshal(output.Bytes(), &report); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if report.Scope != "app" || len(report.Sources) != 4 {
|
||||
t.Fatalf("source report = %#v", report)
|
||||
}
|
||||
if report.Sources[0].Path != "root/config/main.yml" || report.Sources[0].ID != "source-1" {
|
||||
t.Fatalf("main source = %#v", report.Sources[0])
|
||||
}
|
||||
if report.Sources[0].ImportedFrom != nil {
|
||||
t.Fatalf("root source imported_from = %#v, want null", report.Sources[0].ImportedFrom)
|
||||
}
|
||||
imported := report.Sources[1]
|
||||
if imported.Path != "root/shared.yml" || imported.ImportedFrom == nil || imported.ImportedFrom.SourceID != report.Sources[0].ID || imported.ImportedFrom.Line == 0 {
|
||||
t.Fatalf("import source correlation = %#v", imported)
|
||||
}
|
||||
synthetic := report.Sources[2]
|
||||
if synthetic.Kind != string(configkit.SourceKindMapping) || synthetic.Path != "" || synthetic.Layer != string(configkit.LayerUnderlay) || synthetic.UnderlayIndex != 1 {
|
||||
t.Fatalf("synthetic underlay source = %#v", synthetic)
|
||||
}
|
||||
if report.Sources[3].Layer != string(configkit.LayerOverlay) || report.Sources[3].Path != "root/overlays/dev.yml" {
|
||||
t.Fatalf("overlay source = %#v", report.Sources[3])
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigSourcesHandlesEmptyInventoryAllPreflightAndWriterFailure(t *testing.T) {
|
||||
reader := &capturedSourceReader{}
|
||||
newContextCalls, acquireCalls := 0, 0
|
||||
app, err := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
suite := commandkit.ConfigSuite[string]{
|
||||
NewContext: func() string { newContextCalls++; return "context" },
|
||||
Scopes: []commandkit.ConfigScope[string]{{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigSources}, Acquire: func(context.Context, commandkit.ConfigRequest, string) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||
acquireCalls++
|
||||
return commandkit.Acquired[commandkit.ConfigView]{Value: commandkit.ConfigView{Sources: reader}}, nil
|
||||
}}},
|
||||
}
|
||||
if err := commandkit.RegisterConfig(app, suite); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var output bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), []string{"config", "sources", "--scope", "all"}, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeUsage || newContextCalls != 0 || acquireCalls != 0 || output.Len() != 0 {
|
||||
t.Fatalf("all preflight outcome=%#v contexts=%d acquires=%d output=%q", outcome, newContextCalls, acquireCalls, output.String())
|
||||
}
|
||||
outcome = app.Execute(context.Background(), []string{"config", "sources", "--format", "yaml"}, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || output.String() != "scope: app\nsources: []\n" || reader.calls != 1 {
|
||||
t.Fatalf("empty source report outcome=%#v output=%q reader calls=%d", outcome, output.String(), reader.calls)
|
||||
}
|
||||
reader.entries = []configkit.SourceInfo{{ID: "source-1", Label: "label with \"quotes\"", Path: "path with spaces.yml", Layer: configkit.LayerBase, Kind: configkit.SourceKindFile}}
|
||||
var textOutput bytes.Buffer
|
||||
outcome = app.Execute(context.Background(), []string{"config", "sources"}, commandkit.Streams{Out: &textOutput, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(textOutput.String(), `label: "label with \"quotes\""`) || !strings.Contains(textOutput.String(), `path: "path with spaces.yml"`) {
|
||||
t.Fatalf("quoted text source output outcome=%#v output=%q", outcome, textOutput.String())
|
||||
}
|
||||
partial := &configKitShowPartialWriter{failure: io.ErrClosedPipe}
|
||||
outcome = app.Execute(context.Background(), []string{"config", "sources"}, commandkit.Streams{Out: partial, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeOperational || !outcome.ReportIncomplete || !errors.Is(outcome.DeliveryError, io.ErrClosedPipe) {
|
||||
t.Fatalf("source writer failure = %#v", outcome)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigShowWritesNativeSingleScopeAndFramesAggregateYAML(t *testing.T) {
|
||||
const native = "%YAML 1.1\n# authored comment\n---\nvalue: null\nitems: []\nmissing: {}\n"
|
||||
app, err := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var calls []string
|
||||
suite := commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{
|
||||
{Name: "zeta", Operations: []commandkit.ConfigOperation{commandkit.ConfigShow}, Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||
calls = append(calls, "zeta")
|
||||
return commandkit.Acquired[commandkit.ConfigView]{Value: commandkit.ConfigView{Effective: effectiveYAML("%YAML 1.1\n# zeta comment\nvalue: zeta\n")}}, nil
|
||||
}},
|
||||
{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigShow}, Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||
calls = append(calls, "app")
|
||||
return commandkit.Acquired[commandkit.ConfigView]{Value: commandkit.ConfigView{Effective: effectiveYAML(native)}}, nil
|
||||
}},
|
||||
}}
|
||||
if err := commandkit.RegisterConfig(app, suite); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var single bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), []string{"config", "show"}, commandkit.Streams{Out: &single, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || single.String() != native {
|
||||
t.Fatalf("single show outcome=%#v output=%q", outcome, single.String())
|
||||
}
|
||||
calls = nil
|
||||
outcome = app.Execute(context.Background(), []string{"config", "show", "--format", "text"}, commandkit.Streams{Out: &single, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeUsage || len(calls) != 0 {
|
||||
t.Fatalf("text format outcome=%#v acquisitions=%v", outcome, calls)
|
||||
}
|
||||
|
||||
calls = nil
|
||||
var aggregate bytes.Buffer
|
||||
outcome = app.Execute(context.Background(), []string{"config", "show", "--scope", "all"}, commandkit.Streams{Out: &aggregate, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || !reflect.DeepEqual(calls, []string{"app", "zeta"}) {
|
||||
t.Fatalf("aggregate outcome=%#v acquisitions=%v", outcome, calls)
|
||||
}
|
||||
if !strings.Contains(aggregate.String(), "%YAML 1.1\n# authored comment\n---\n# scope: app\n") || !strings.Contains(aggregate.String(), "...\n%YAML 1.1\n# zeta comment\n---\n# scope: zeta\nvalue: zeta\n") {
|
||||
t.Fatalf("aggregate framing = %q", aggregate.String())
|
||||
}
|
||||
decoder := yaml.NewDecoder(bytes.NewReader(aggregate.Bytes()))
|
||||
var first, second map[string]any
|
||||
if err := decoder.Decode(&first); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := decoder.Decode(&second); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var extra any
|
||||
if err := decoder.Decode(&extra); err != io.EOF {
|
||||
t.Fatalf("third YAML document decode error = %v, value=%#v", err, extra)
|
||||
}
|
||||
if first["value"] != nil || first["items"] == nil || first["missing"] == nil || second["value"] != "zeta" {
|
||||
t.Fatalf("decoded documents = %#v and %#v", first, second)
|
||||
}
|
||||
if _, exists := first["absent"]; exists {
|
||||
t.Fatalf("absent field was added to native document: %#v", first)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigShowAcquiresAllBeforeOutputAndTracksWriterFailures(t *testing.T) {
|
||||
failure := errors.New("second scope failed")
|
||||
app, err := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
calls := 0
|
||||
suite := commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{
|
||||
{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigShow}, Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||
calls++
|
||||
return commandkit.Acquired[commandkit.ConfigView]{Value: commandkit.ConfigView{Effective: effectiveYAML("value: app\n")}}, nil
|
||||
}},
|
||||
{Name: "promptkit", Operations: []commandkit.ConfigOperation{commandkit.ConfigShow}, Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||
calls++
|
||||
return commandkit.Acquired[commandkit.ConfigView]{Diagnostics: []commandkit.Diagnostic{commandkit.MessageDiagnostic(configkit.SeverityWarning, "provider warning")}}, failure
|
||||
}},
|
||||
}}
|
||||
if err := commandkit.RegisterConfig(app, suite); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var output bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), []string{"config", "show", "--scope", "all"}, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||
if !errors.Is(outcome.Cause, failure) || outcome.ReportIncomplete || output.Len() != 0 || calls != 2 || len(outcome.Diagnostics) < 1 || outcome.Diagnostics[0].Severity != configkit.SeverityWarning {
|
||||
t.Fatalf("acquisition failure outcome=%#v output=%q calls=%d", outcome, output.String(), calls)
|
||||
}
|
||||
|
||||
app, _ = commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigShow}, Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||
return commandkit.Acquired[commandkit.ConfigView]{Value: commandkit.ConfigView{Effective: effectiveYAML("value: app\n")}}, nil
|
||||
}}}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
partial := &configKitShowPartialWriter{failure: io.ErrClosedPipe}
|
||||
outcome = app.Execute(context.Background(), []string{"config", "show", "--scope", "all"}, commandkit.Streams{Out: partial, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeOperational || !outcome.ReportIncomplete || !errors.Is(outcome.Cause, io.ErrClosedPipe) || !errors.Is(outcome.DeliveryError, io.ErrClosedPipe) || len(partial.written) == 0 {
|
||||
t.Fatalf("writer failure outcome = %#v", outcome)
|
||||
}
|
||||
}
|
||||
|
||||
type configKitShowPartialWriter struct {
|
||||
written []byte
|
||||
failure error
|
||||
}
|
||||
|
||||
func (writer *configKitShowPartialWriter) Write(data []byte) (int, error) {
|
||||
n := len(data)
|
||||
if n > 4 {
|
||||
n = 4
|
||||
}
|
||||
writer.written = append(writer.written, data[:n]...)
|
||||
return n, writer.failure
|
||||
}
|
||||
|
||||
func TestConfigKitScopeUsesDefaultsOverlayAndRecordedResolution(t *testing.T) {
|
||||
type configuration struct {
|
||||
Mode string `yaml:"mode"`
|
||||
Defaulted string `yaml:"defaulted"`
|
||||
}
|
||||
filesystem := configkit.NewFilesystem("fixture", fstest.MapFS{
|
||||
"config/main.yml": &fstest.MapFile{Data: []byte("mode: base\n")},
|
||||
"config/overlays/dev.yml": &fstest.MapFile{Data: []byte("mode: overlay\n")},
|
||||
})
|
||||
definition := configkit.Definition[configuration]{
|
||||
Defaults: func() configuration { return configuration{Defaulted: "from default"} },
|
||||
Resolve: func(input *configuration, resolution *configkit.Resolution) error {
|
||||
resolution.Record(configkit.Keys("mode"), configkit.ChangeNormalization, "normalized for use")
|
||||
input.Mode = strings.ToUpper(input.Mode)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
var received commandkit.Selection
|
||||
scope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "Application settings.", func(_ context.Context, selection commandkit.Selection, _ struct{}) (*configkit.Result[configuration], error) {
|
||||
received = selection
|
||||
return configkit.LoadLayered(configkit.Input{Source: filesystem.File(selection.Config.Value)}, configkit.LayeredOptions[configuration]{
|
||||
Definition: definition,
|
||||
Overlay: selection.Overlay.Value,
|
||||
OverlayFilesystem: &configkit.FilesystemContext{Root: "config", BaseDir: "config", FS: &filesystem},
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
selection := commandkit.Selection{
|
||||
Config: commandkit.Supplied[string]{Value: "config/main.yml", Set: true},
|
||||
Overlay: commandkit.Supplied[string]{Value: "dev", Set: true},
|
||||
}
|
||||
acquired, err := scope.Acquire(context.Background(), commandkit.ConfigRequest{Operation: commandkit.ConfigValidate, Selection: selection}, struct{}{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(received, selection) {
|
||||
t.Fatalf("loader selection = %#v, want %#v", received, selection)
|
||||
}
|
||||
if acquired.Value.Effective == nil || acquired.Value.Sources == nil || acquired.Value.Explanation == nil {
|
||||
t.Fatalf("ConfigKit result facets = %#v", acquired.Value)
|
||||
}
|
||||
if got := acquired.Value.Sources.Sources(); len(got) < 2 {
|
||||
t.Fatalf("sources = %#v, want base and overlay", got)
|
||||
}
|
||||
var effective bytes.Buffer
|
||||
if err := acquired.Value.Effective.WriteEffective(&effective); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(effective.String(), "mode: OVERLAY") || !strings.Contains(effective.String(), "defaulted: from default") {
|
||||
t.Fatalf("effective configuration = %q", effective.String())
|
||||
}
|
||||
if explanation, ok := acquired.Value.Explanation.Explain(configkit.Keys("mode")); !ok || len(explanation.History) == 0 {
|
||||
t.Fatalf("resolution explanation = %#v, present=%t", explanation, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigKitScopePreservesAbsentAndEmptySelections(t *testing.T) {
|
||||
type configuration struct {
|
||||
Value string `yaml:"value"`
|
||||
}
|
||||
var seen []commandkit.Selection
|
||||
scope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "", func(_ context.Context, selection commandkit.Selection, _ struct{}) (*configkit.Result[configuration], error) {
|
||||
seen = append(seen, selection)
|
||||
return configkit.Load(configkit.NoSource(), configkit.Options[configuration]{Definition: configkit.Definition[configuration]{Defaults: func() configuration { return configuration{Value: "default"} }}})
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = scope.Acquire(context.Background(), commandkit.ConfigRequest{}, struct{}{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
empty := commandkit.Selection{Config: commandkit.Supplied[string]{Set: true}, Overlay: commandkit.Supplied[string]{Set: true}}
|
||||
_, err = scope.Acquire(context.Background(), commandkit.ConfigRequest{Selection: empty}, struct{}{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []commandkit.Selection{{}, empty}
|
||||
if !reflect.DeepEqual(seen, want) {
|
||||
t.Fatalf("loader selections = %#v, want %#v", seen, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigKitScopeRequiresLoaderAndNonNilResult(t *testing.T) {
|
||||
if _, err := commandkit.NewConfigKitScope[struct{}, struct{}]("app", "", nil); err == nil {
|
||||
t.Fatal("accepted a nil ConfigKit loader")
|
||||
}
|
||||
scope, err := commandkit.NewConfigKitScope[struct{}, struct{}]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[struct{}], error) {
|
||||
return nil, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := scope.Acquire(context.Background(), commandkit.ConfigRequest{}, struct{}{}); err == nil || !strings.Contains(err.Error(), "nil result") {
|
||||
t.Fatalf("nil result error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigKitLoadErrorRetainsIdentityAndStructuredDiagnostics(t *testing.T) {
|
||||
type configuration struct {
|
||||
Name string `yaml:"name"`
|
||||
}
|
||||
var loadError error
|
||||
scope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[configuration], error) {
|
||||
_, loadError = configkit.Load(configkit.Mapping("bad source", map[string]any{"unknown": true}), configkit.Options[configuration]{Definition: configkit.Definition[configuration]{}})
|
||||
return nil, loadError
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
acquired, acquireError := scope.Acquire(context.Background(), commandkit.ConfigRequest{}, struct{}{})
|
||||
var publicLoadError *configkit.LoadError
|
||||
if acquireError != loadError || !errors.As(acquireError, &publicLoadError) {
|
||||
t.Fatalf("load error identity = %v (original %v), public type=%T", acquireError, loadError, acquireError)
|
||||
}
|
||||
if len(acquired.Diagnostics) != 1 || acquired.Diagnostics[0].Severity != configkit.SeverityError {
|
||||
t.Fatalf("structured 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("grouped diagnostics = %q", rendered.String())
|
||||
}
|
||||
|
||||
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{scope}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var output, diagnostic bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), []string{"config", "validate"}, commandkit.Streams{Out: &output, Err: &diagnostic})
|
||||
if outcome.Cause != loadError || len(outcome.Diagnostics) != 1 || strings.Count(diagnostic.String(), `Configuration scope "app":`) != 1 || output.Len() != 0 {
|
||||
t.Fatalf("presented load failure outcome=%#v output=%q diagnostics=%q", outcome, output.String(), diagnostic.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigValidationEmitsSuccessAsScopesCompleteAndStopsAtFailure(t *testing.T) {
|
||||
app, err := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var calls []string
|
||||
newContextCalls := 0
|
||||
failure := errors.New("promptkit could not load")
|
||||
suite := commandkit.ConfigSuite[string]{
|
||||
NewContext: func() string { newContextCalls++; return "runtime" },
|
||||
Scopes: []commandkit.ConfigScope[string]{
|
||||
{Name: "zeta", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: func(context.Context, commandkit.ConfigRequest, string) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||
calls = append(calls, "zeta")
|
||||
return commandkit.Acquired[commandkit.ConfigView]{}, nil
|
||||
}},
|
||||
{Name: "promptkit", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: func(context.Context, commandkit.ConfigRequest, string) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||
calls = append(calls, "promptkit")
|
||||
return commandkit.Acquired[commandkit.ConfigView]{}, failure
|
||||
}},
|
||||
{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: func(context.Context, commandkit.ConfigRequest, string) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||
calls = append(calls, "app")
|
||||
return commandkit.Acquired[commandkit.ConfigView]{}, nil
|
||||
}},
|
||||
},
|
||||
}
|
||||
if err := commandkit.RegisterConfig(app, suite); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var output, diagnostic bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), []string{"config", "validate", "--scope", "all"}, commandkit.Streams{Out: &output, Err: &diagnostic})
|
||||
if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.Cause, failure) || !outcome.ReportIncomplete {
|
||||
t.Fatalf("validation outcome = %#v", outcome)
|
||||
}
|
||||
if !reflect.DeepEqual(calls, []string{"app", "promptkit"}) || newContextCalls != 1 {
|
||||
t.Fatalf("acquisitions = %v, NewContext calls=%d", calls, newContextCalls)
|
||||
}
|
||||
if output.String() != "app: valid\n" || !strings.Contains(diagnostic.String(), failure.Error()) {
|
||||
t.Fatalf("output=%q diagnostics=%q", output.String(), diagnostic.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigValidationYAMLWarningsAndShortWrites(t *testing.T) {
|
||||
type configuration struct {
|
||||
Name string `yaml:"name"`
|
||||
}
|
||||
warningScope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[configuration], error) {
|
||||
return configkit.Load(configkit.NoSource(), configkit.Options[configuration]{Definition: configkit.Definition[configuration]{
|
||||
Validate: func(_ *configuration, validation *configkit.Validation) {
|
||||
validation.Warning(configkit.Keys("name"), "uses a demonstration name")
|
||||
},
|
||||
}})
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{warningScope}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var output, diagnostic bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), []string{"config", "validate", "--format", "yaml"}, commandkit.Streams{Out: &output, Err: &diagnostic})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || outcome.Status != 0 || outcome.DiagnosticDelivery != commandkit.DeliveryComplete {
|
||||
t.Fatalf("warning validation outcome = %#v", outcome)
|
||||
}
|
||||
if output.String() != "scopes:\n - name: app\n valid: true\n" || !strings.Contains(diagnostic.String(), "uses a demonstration name") {
|
||||
t.Fatalf("output=%q diagnostics=%q", output.String(), diagnostic.String())
|
||||
}
|
||||
|
||||
outcome = app.Execute(context.Background(), []string{"config", "validate"}, commandkit.Streams{Out: configKitPartialWriter{}, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeOperational || !outcome.ReportIncomplete || !errors.Is(outcome.Cause, io.ErrShortWrite) || !errors.Is(outcome.DeliveryError, io.ErrShortWrite) {
|
||||
t.Fatalf("report short write outcome = %#v", outcome)
|
||||
}
|
||||
var delivered bytes.Buffer
|
||||
outcome = app.Execute(context.Background(), []string{"config", "validate"}, commandkit.Streams{Out: &delivered, Err: configKitPartialWriter{}})
|
||||
if outcome.DiagnosticDelivery != commandkit.DeliveryIncomplete || !errors.Is(outcome.DeliveryError, io.ErrShortWrite) {
|
||||
t.Fatalf("diagnostic short write outcome = %#v", outcome)
|
||||
}
|
||||
}
|
||||
|
||||
type configKitPartialWriter struct{}
|
||||
|
||||
func (configKitPartialWriter) Write(data []byte) (int, error) {
|
||||
if len(data) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return len(data) - 1, nil
|
||||
}
|
||||
|
||||
func TestValidationReportDeliveryErrorsAtEachWriteBoundary(t *testing.T) {
|
||||
reportErr := errors.New("report failed")
|
||||
diagnosticErr := errors.New("diagnostic failed")
|
||||
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
acquire := func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||
return commandkit.Acquired[commandkit.ConfigView]{}, nil
|
||||
}
|
||||
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{
|
||||
{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: acquire},
|
||||
{Name: "promptkit", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: acquire},
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, format := range []string{"text", "yaml"} {
|
||||
writes := 2
|
||||
if format == "yaml" {
|
||||
writes = 3
|
||||
}
|
||||
for at := 1; at <= writes; at++ {
|
||||
for _, brokenErr := range []bool{false, true} {
|
||||
out := &validationBoundaryWriter{failAt: at, err: reportErr}
|
||||
errStream := io.Writer(io.Discard)
|
||||
if brokenErr {
|
||||
errStream = failingWriter{diagnosticErr}
|
||||
}
|
||||
result := app.Execute(context.Background(), []string{"config", "validate", "--scope", "all", "--format", format}, commandkit.Streams{Out: out, Err: errStream})
|
||||
if result.Kind != commandkit.OutcomeOperational || !result.ReportIncomplete || !errors.Is(result.Cause, reportErr) || !errors.Is(result.DeliveryError, reportErr) || (brokenErr && !errors.Is(result.DeliveryError, diagnosticErr)) {
|
||||
t.Fatalf("format=%s at=%d brokenErr=%t result=%#v", format, at, brokenErr, result)
|
||||
}
|
||||
if at > 1 && out.written.Len() == 0 {
|
||||
t.Fatal("lost completed prefix")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
failedApp, _ := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err := commandkit.RegisterConfig(failedApp, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||
return commandkit.Acquired[commandkit.ConfigView]{}, reportErr
|
||||
}}}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result := failedApp.Execute(context.Background(), []string{"config", "validate"}, commandkit.Streams{})
|
||||
if !errors.Is(result.Cause, reportErr) || result.DeliveryError != nil {
|
||||
t.Fatalf("acquisition failure=%#v", result)
|
||||
}
|
||||
}
|
||||
|
||||
type validationBoundaryWriter struct {
|
||||
failAt, calls int
|
||||
err error
|
||||
written bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *validationBoundaryWriter) Write(p []byte) (int, error) {
|
||||
w.calls++
|
||||
if w.calls == w.failAt {
|
||||
return 0, w.err
|
||||
}
|
||||
return w.written.Write(p)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||
"gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig"
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
)
|
||||
|
||||
@@ -19,38 +20,28 @@ type greetInput struct {
|
||||
Tags commandkit.Supplied[[]string]
|
||||
}
|
||||
|
||||
func loadAppConfiguration(context.Context, commandkit.Selection, struct{}) (*configkit.Result[appConfiguration], error) {
|
||||
return configkit.Load[appConfiguration](configkit.Bytes("simple example", []byte("greeting: hello\n")))
|
||||
func loadAppConfiguration(_ context.Context, _ ckconfig.Selection) (*configkit.Result[appConfiguration], error) {
|
||||
return configkit.Load[appConfiguration](configkit.Mapping("simple application settings", map[string]any{
|
||||
"greeting": "Hello",
|
||||
}))
|
||||
}
|
||||
|
||||
func newApp() (*commandkit.App, error) {
|
||||
app, err := commandkit.New(commandkit.Identity{Name: "simple-example", Version: "1.0"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scope, err := commandkit.NewConfigKitScope[struct{}, appConfiguration]("app", "Application settings.", loadAppConfiguration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{scope}}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
countDefault := 1
|
||||
tagDefaults := []string{"example"}
|
||||
greet, err := commandkit.NewCommand(commandkit.CommandSpec[greetInput]{
|
||||
greet := commandkit.NewCommand(commandkit.CommandSpec[greetInput]{
|
||||
Name: "greet",
|
||||
Summary: "Show flag presence and repeated values.",
|
||||
Flags: []commandkit.Flag[greetInput]{
|
||||
commandkit.ValueFlag[greetInput](commandkit.FlagMeta{Name: "count", Usage: "Greeting count."}, &countDefault, strconv.Atoi, func(input *greetInput, value commandkit.Supplied[int]) {
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "count", Usage: "Greeting count."}, &countDefault, strconv.Atoi, func(input *greetInput, value commandkit.Supplied[int]) {
|
||||
input.Count = value
|
||||
}),
|
||||
commandkit.RepeatFlag[greetInput](commandkit.FlagMeta{Name: "tag", Usage: "A repeatable label."}, tagDefaults, func(value string) (string, error) { return value, nil }, func(input *greetInput, value commandkit.Supplied[[]string]) {
|
||||
commandkit.RepeatFlag(commandkit.FlagMeta{Name: "tag", Usage: "A repeatable label."}, tagDefaults, func(value string) (string, error) { return value, nil }, func(input *greetInput, value commandkit.Supplied[[]string]) {
|
||||
input.Tags = value
|
||||
}),
|
||||
},
|
||||
Run: func(invocation commandkit.Invocation, input greetInput) commandkit.Outcome {
|
||||
configuration, err := loadAppConfiguration(invocation.Context, commandkit.Selection{}, struct{}{})
|
||||
configuration, err := loadAppConfiguration(invocation.Context, ckconfig.Selection{})
|
||||
if err != nil {
|
||||
return commandkit.Failure(err)
|
||||
}
|
||||
@@ -68,13 +59,11 @@ func newApp() (*commandkit.App, error) {
|
||||
return commandkit.Success()
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := app.Register(greet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return app, nil
|
||||
return commandkit.New(
|
||||
commandkit.Identity{Name: "simple-example", Version: "1.0"},
|
||||
ckconfig.AppCommand(loadAppConfiguration),
|
||||
greet,
|
||||
)
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package ckconfig_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||
"gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig"
|
||||
)
|
||||
|
||||
func TestParentHelpDiscoversMetadataOnlyGroups(t *testing.T) {
|
||||
leaf := commandkit.Group("notes", "Read notes metadata.")
|
||||
parent := commandkit.Group("topics", "Browse topic metadata.", leaf)
|
||||
app, err := commandkit.New(commandkit.Identity{Name: "example"}, parent)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loads := 0
|
||||
if err := ckconfig.RegisterConfig(app, ckconfig.ConfigSuite[struct{}]{Scopes: []ckconfig.ConfigScope[struct{}]{{Name: "app", Operations: []ckconfig.ConfigOperation{ckconfig.ConfigValidate}, Acquire: func(context.Context, ckconfig.ConfigRequest, struct{}) (commandkit.Acquired[ckconfig.ConfigView], error) {
|
||||
loads++
|
||||
return commandkit.Acquired[ckconfig.ConfigView]{}, nil
|
||||
}}}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
args []string
|
||||
content string
|
||||
}{
|
||||
{nil, "Browse topic metadata."},
|
||||
{[]string{"topics"}, "Read notes metadata."},
|
||||
{[]string{"topics", "--help"}, "Read notes metadata."},
|
||||
{[]string{"help", "topics"}, "Read notes metadata."},
|
||||
{[]string{"topics", "notes"}, "Read notes metadata."},
|
||||
{[]string{"help", "topics", "notes"}, "Read notes metadata."},
|
||||
} {
|
||||
var out bytes.Buffer
|
||||
result := app.Execute(context.Background(), tc.args, commandkit.Streams{Out: &out})
|
||||
if result.Kind != commandkit.OutcomeSuccess || !strings.Contains(out.String(), tc.content) || loads != 0 {
|
||||
t.Fatalf("%v: result=%#v output=%q loads=%d", tc.args, result, out.String(), loads)
|
||||
}
|
||||
if len(tc.args) == 1 && (!strings.Contains(out.String(), "Available Commands:") || !strings.Contains(out.String(), "[command]")) {
|
||||
t.Fatalf("missing child inventory/usage: %q", out.String())
|
||||
}
|
||||
}
|
||||
var diagnostic bytes.Buffer
|
||||
result := app.Execute(context.Background(), []string{"topics", "unknown"}, commandkit.Streams{Err: &diagnostic})
|
||||
if result.Kind != commandkit.OutcomeUsage || !strings.Contains(diagnostic.String(), "[command]") {
|
||||
t.Fatalf("result=%#v diagnostic=%q", result, diagnostic.String())
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
package ckconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var performanceFramedBytes []byte
|
||||
|
||||
func BenchmarkPerformanceYAMLFraming(b *testing.B) {
|
||||
fixtures := []struct {
|
||||
name string
|
||||
scope string
|
||||
native []byte
|
||||
}{
|
||||
{name: "4k_ordinary", scope: "app", native: performanceNativeDocument(4<<10, false)},
|
||||
{name: "1m_directive", scope: "promptkit", native: performanceNativeDocument(1<<20, true)},
|
||||
}
|
||||
for _, fixture := range fixtures {
|
||||
b.Run("framing/"+fixture.name, func(b *testing.B) {
|
||||
framed, hasDirective := frameEffectiveYAML(fixture.scope, fixture.native)
|
||||
if len(framed) <= len(fixture.native) || hasDirective != strings.Contains(fixture.name, "directive") {
|
||||
b.Fatalf("framing fixture result has unexpected size or directive classification: native=%d framed=%d", len(fixture.native), len(framed))
|
||||
}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
performanceFramedBytes, _ = frameEffectiveYAML(fixture.scope, fixture.native)
|
||||
}
|
||||
})
|
||||
b.Run("aggregate/"+fixture.name, func(b *testing.B) {
|
||||
app := performanceShowApp(b, fixture.native, fixture.native)
|
||||
args := []string{"config", "show", "--scope", "all"}
|
||||
verifyConfigReport(b, app, args, len(fixture.native)*2)
|
||||
streams := Streams{Out: io.Discard, Err: io.Discard}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if outcome := app.Execute(context.Background(), args, streams); outcome.Kind != OutcomeSuccess {
|
||||
b.Fatalf("aggregate config show outcome = %#v", outcome)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
single := performanceNativeDocument(4<<10, false)
|
||||
app := performanceShowApp(b, single)
|
||||
args := []string{"config", "show"}
|
||||
b.Run("single_scope/4k_ordinary", func(b *testing.B) {
|
||||
verifyConfigReport(b, app, args, len(single))
|
||||
streams := Streams{Out: io.Discard, Err: io.Discard}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if outcome := app.Execute(context.Background(), args, streams); outcome.Kind != OutcomeSuccess {
|
||||
b.Fatalf("single-scope config show outcome = %#v", outcome)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkPerformanceConfigScopes(b *testing.B) {
|
||||
for _, scopeCount := range []int{2, 32} {
|
||||
suite := performanceConfigSuite(scopeCount, []ConfigOperation{ConfigValidate, ConfigShow})
|
||||
app := performanceConfigApp(b, suite)
|
||||
selected := fmt.Sprintf("scope%02d", scopeCount-1)
|
||||
for _, operation := range []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{name: "discovery", args: []string{"config", "scopes"}},
|
||||
{name: "named_selection", args: []string{"config", "validate", "--scope", selected}},
|
||||
{name: "aggregate_validation", args: []string{"config", "validate", "--scope", "all"}},
|
||||
} {
|
||||
b.Run(fmt.Sprintf("%s/scopes_%d", operation.name, scopeCount), func(b *testing.B) {
|
||||
verifyConfigReport(b, app, operation.args, 1)
|
||||
streams := Streams{Out: io.Discard, Err: io.Discard}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if outcome := app.Execute(context.Background(), operation.args, streams); outcome.Kind != OutcomeSuccess {
|
||||
b.Fatalf("configuration operation outcome = %#v", outcome)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPerformanceConfigRegistration(b *testing.B) {
|
||||
for _, selectorCount := range []int{4, 64} {
|
||||
suite := performanceSelectorSuite(selectorCount)
|
||||
performanceConfigApp(b, suite)
|
||||
b.Run(fmt.Sprintf("selectors_%d", selectorCount), func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
app, err := New(Identity{Name: "bench"})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if err := RegisterConfig(app, suite); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func performanceNativeDocument(targetBytes int, directive bool) []byte {
|
||||
preamble := "# generated native configuration\n"
|
||||
if directive {
|
||||
preamble = "%YAML 1.2\n---\n"
|
||||
}
|
||||
var body strings.Builder
|
||||
body.Grow(targetBytes)
|
||||
for index := 0; len(preamble)+body.Len() < targetBytes; index++ {
|
||||
fmt.Fprintf(&body, "key%06d: value\n", index)
|
||||
}
|
||||
return []byte(preamble + body.String())
|
||||
}
|
||||
|
||||
type performanceEffectiveWriter []byte
|
||||
|
||||
func (document performanceEffectiveWriter) WriteEffective(writer io.Writer) error {
|
||||
_, err := writer.Write(document)
|
||||
return err
|
||||
}
|
||||
|
||||
func performanceShowApp(tb testing.TB, documents ...[]byte) *App {
|
||||
tb.Helper()
|
||||
scopes := make([]ConfigScope[struct{}], len(documents))
|
||||
for index, document := range documents {
|
||||
name := "app"
|
||||
if index > 0 {
|
||||
name = fmt.Sprintf("scope%02d", index)
|
||||
}
|
||||
writer := performanceEffectiveWriter(document)
|
||||
scopes[index] = ConfigScope[struct{}]{
|
||||
Name: name,
|
||||
Operations: []ConfigOperation{ConfigShow},
|
||||
Acquire: func(context.Context, ConfigRequest, struct{}) (Acquired[ConfigView], error) {
|
||||
return Acquired[ConfigView]{Value: ConfigView{Effective: writer}}, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
suite := ConfigSuite[struct{}]{Scopes: scopes}
|
||||
return performanceConfigApp(tb, suite)
|
||||
}
|
||||
|
||||
func performanceConfigSuite(scopeCount int, operations []ConfigOperation) ConfigSuite[struct{}] {
|
||||
scopes := make([]ConfigScope[struct{}], scopeCount)
|
||||
for index := range scopes {
|
||||
name := "app"
|
||||
if index > 0 {
|
||||
name = fmt.Sprintf("scope%02d", index)
|
||||
}
|
||||
scopes[index] = ConfigScope[struct{}]{
|
||||
Name: name,
|
||||
Operations: append([]ConfigOperation(nil), operations...),
|
||||
Acquire: func(context.Context, ConfigRequest, struct{}) (Acquired[ConfigView], error) {
|
||||
return Acquired[ConfigView]{}, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
return ConfigSuite[struct{}]{Scopes: scopes}
|
||||
}
|
||||
|
||||
func performanceSelectorSuite(selectorCount int) ConfigSuite[struct{}] {
|
||||
suite := performanceConfigSuite(1, []ConfigOperation{ConfigValidate})
|
||||
suite.Selectors = make([]Selector[struct{}], selectorCount)
|
||||
for index := range suite.Selectors {
|
||||
flag := ValueFlag(FlagMeta{Name: fmt.Sprintf("selector%02d", index)}, (*string)(nil), parseString, func(*struct{}, Supplied[string]) {})
|
||||
suite.Selectors[index] = Selector[struct{}]{Binding: flag, Scopes: []string{"app"}}
|
||||
}
|
||||
return suite
|
||||
}
|
||||
|
||||
func performanceConfigApp(tb testing.TB, suite ConfigSuite[struct{}]) *App {
|
||||
tb.Helper()
|
||||
app, err := New(Identity{Name: "bench"})
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
if err := RegisterConfig(app, suite); err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
return app
|
||||
}
|
||||
|
||||
func verifyConfigReport(tb testing.TB, app *App, args []string, minimumBytes int) {
|
||||
tb.Helper()
|
||||
var output bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), args, Streams{Out: &output, Err: io.Discard})
|
||||
if outcome.Kind != OutcomeSuccess || output.Len() < minimumBytes {
|
||||
tb.Fatalf("configuration report outcome = %#v, output bytes %d, want at least %d", outcome, output.Len(), minimumBytes)
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package commandkit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEffectiveYAMLFramingPreservesOneNativeDocument(t *testing.T) {
|
||||
for _, native := range []string{
|
||||
"--- {setting: value}\n", "---\t{setting: value}\n", "--- value\n",
|
||||
"--- |\n first\n second\n", "--- # authored\nsetting: value\n",
|
||||
"setting: value\n", "%YAML 1.1\n--- {setting: value}\n",
|
||||
} {
|
||||
t.Run(native, func(t *testing.T) {
|
||||
var want any
|
||||
if err := yaml.Unmarshal([]byte(native), &want); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
framed, _ := frameEffectiveYAML("app", []byte(native))
|
||||
withoutLabel := bytes.Replace(framed, []byte("# scope: app\n"), nil, 1)
|
||||
if !strings.HasPrefix(native, "---") && !strings.HasPrefix(native, "%") {
|
||||
withoutLabel = bytes.TrimPrefix(withoutLabel, []byte("---\n"))
|
||||
}
|
||||
if !bytes.Equal(withoutLabel, []byte(native)) {
|
||||
t.Fatalf("native content changed: %q", framed)
|
||||
}
|
||||
if !bytes.Contains(framed, []byte("# scope: app\n")) {
|
||||
t.Fatalf("missing label: %q", framed)
|
||||
}
|
||||
decoder := yaml.NewDecoder(bytes.NewReader(framed))
|
||||
var got any
|
||||
if err := decoder.Decode(&got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got=%#v want=%#v", got, want)
|
||||
}
|
||||
if err := decoder.Decode(new(any)); err != io.EOF {
|
||||
t.Fatalf("extra document: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveYAMLFramingPreservesBoundariesAndDirectives(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
scope string
|
||||
native string
|
||||
directive bool
|
||||
label string
|
||||
}{
|
||||
{name: "no trailing newline", scope: "app", native: "value: plain", label: "# scope: app\n"},
|
||||
{name: "CRLF", scope: "app", native: "# authored\r\n---\r\nvalue: yes\r\n", label: "# scope: app\n"},
|
||||
{name: "directive preamble", scope: "app", native: "%YAML 1.1\n# authored\n---\nvalue: yes", directive: true, label: "# scope: app\n"},
|
||||
{name: "control character label", scope: "line\nbreak", native: "value: yes", label: "# scope: \"line\\nbreak\"\n"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var want any
|
||||
if err := yaml.Unmarshal([]byte(test.native), &want); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
framed, hasDirective := frameEffectiveYAML(test.scope, []byte(test.native))
|
||||
if hasDirective != test.directive {
|
||||
t.Fatalf("hasDirective = %t, want %t", hasDirective, test.directive)
|
||||
}
|
||||
withoutLabel := bytes.Replace(framed, []byte(test.label), nil, 1)
|
||||
if !strings.HasPrefix(test.native, "---") && !strings.HasPrefix(test.native, "%") {
|
||||
withoutLabel = bytes.TrimPrefix(withoutLabel, []byte("---\n"))
|
||||
}
|
||||
if !bytes.Equal(withoutLabel, []byte(test.native)) {
|
||||
t.Fatalf("native content changed: got %q want %q", withoutLabel, test.native)
|
||||
}
|
||||
if !bytes.Contains(framed, []byte(test.label)) {
|
||||
t.Fatalf("missing scope label %q in %q", test.label, framed)
|
||||
}
|
||||
decoder := yaml.NewDecoder(bytes.NewReader(framed))
|
||||
var first any
|
||||
if err := decoder.Decode(&first); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(first, want) {
|
||||
t.Fatalf("decoded value = %#v, want %#v", first, want)
|
||||
}
|
||||
if err := decoder.Decode(new(any)); err != io.EOF {
|
||||
t.Fatalf("unexpected extra YAML document: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user