-
Notifications
You must be signed in to change notification settings - Fork 14
feat(config): add Validate() method for startup validation #2763
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
f183c2f
e56ab82
35bc20e
962a95b
f635813
4be0c65
9bcf488
c17748f
665eb44
dd2bd62
ef87d7f
cb45d3b
b61c234
cfc226f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| // Copyright 2026 Blink Labs Software | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| package main | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/spf13/cobra" | ||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/blinklabs-io/dingo/internal/config" | ||
| ) | ||
|
|
||
| // newTestRootCmd wires the real subcommands under a root command, | ||
| // mirroring main() so the command-classification helpers see the same | ||
| // tree. | ||
| func newTestRootCmd() *cobra.Command { | ||
| root := &cobra.Command{Use: "dingo"} | ||
| root.AddCommand( | ||
| serveCommand(), | ||
| loadCommand(), | ||
| listCommand(), | ||
| versionCommand(), | ||
| mithrilCommand(), | ||
| syncCommand(), | ||
| ) | ||
| return root | ||
| } | ||
|
|
||
| func findCmd(t *testing.T, root *cobra.Command, path ...string) *cobra.Command { | ||
| t.Helper() | ||
| if len(path) == 0 { | ||
| return root | ||
| } | ||
| cmd, _, err := root.Find(path) | ||
| require.NoError(t, err) | ||
| require.Equal(t, path[len(path)-1], cmd.Name()) | ||
| return cmd | ||
| } | ||
|
|
||
| func TestEffectiveRunMode(t *testing.T) { | ||
| root := newTestRootCmd() | ||
| tests := []struct { | ||
| name string | ||
| path []string | ||
| runMode config.RunMode | ||
| want config.RunMode | ||
| }{ | ||
| {"bare serve", nil, config.RunModeServe, config.RunModeServe}, | ||
| {"bare load", nil, config.RunModeLoad, config.RunModeLoad}, | ||
| {"bare dev", nil, config.RunModeDev, config.RunModeDev}, | ||
| {"bare empty defaults to serve", nil, "", config.RunModeServe}, | ||
| // An invalid configured runMode falls through to serve (matching | ||
| // rootCmd's dispatch default) so serving-listener checks still | ||
| // apply; the invalid mode is reported separately by Validate. | ||
| {"bare invalid falls back to serve", nil, "batch", config.RunModeServe}, | ||
| // Subcommands run a fixed operation regardless of configured runMode. | ||
| { | ||
| "serve subcommand ignores load config", | ||
| []string{"serve"}, | ||
| config.RunModeLoad, | ||
| config.RunModeServe, | ||
| }, | ||
| { | ||
| "load subcommand", | ||
| []string{"load"}, | ||
| config.RunModeServe, | ||
| config.RunModeLoad, | ||
| }, | ||
| { | ||
| "sync is a utility", | ||
| []string{"sync"}, | ||
| config.RunModeServe, | ||
| config.RunModeUtility, | ||
| }, | ||
| { | ||
| "mithril list is a utility", | ||
| []string{"mithril", "list"}, | ||
| config.RunModeServe, | ||
| config.RunModeUtility, | ||
| }, | ||
| { | ||
| "mithril sync is a utility", | ||
| []string{"mithril", "sync"}, | ||
| config.RunModeServe, | ||
| config.RunModeUtility, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| cmd := findCmd(t, root, tt.path...) | ||
| top := topLevelCommand(cmd) | ||
| got := effectiveRunMode(top, &config.Config{RunMode: tt.runMode}) | ||
| require.Equal(t, tt.want, got) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestIsInformationalCommand(t *testing.T) { | ||
| root := newTestRootCmd() | ||
| tests := []struct { | ||
| name string | ||
| path []string | ||
| want bool | ||
| }{ | ||
| {"bare root", nil, false}, | ||
| {"version", []string{"version"}, true}, | ||
| {"list", []string{"list"}, true}, | ||
| {"serve", []string{"serve"}, false}, | ||
| {"sync", []string{"sync"}, false}, | ||
| // Nested `mithril list` must not be mistaken for top-level `list`. | ||
| {"mithril list", []string{"mithril", "list"}, false}, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| cmd := findCmd(t, root, tt.path...) | ||
| require.Equal( | ||
| t, | ||
| tt.want, | ||
| isInformationalCommand(topLevelCommand(cmd)), | ||
| ) | ||
| }) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| // Copyright 2026 Blink Labs Software | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| package main | ||
|
|
||
| import ( | ||
| "maps" | ||
| "testing" | ||
|
|
||
| "github.com/blinklabs-io/dingo/chainsync" | ||
| "github.com/blinklabs-io/dingo/internal/config" | ||
| "github.com/blinklabs-io/dingo/mithril" | ||
| ) | ||
|
|
||
| // internal/config cannot import chainsync or mithril without pulling | ||
| // node subsystems into the config package, so the accepted-value sets | ||
| // it validates against (config.AcceptedChainsyncStrategies and | ||
| // config.AcceptedMithrilBackends) are duplicated from those downstream | ||
| // parsers. These parity tests live in cmd/dingo, which can import all | ||
| // three, and guard against drift in both directions: | ||
| // | ||
| // - forward: every value config accepts must be accepted by the real | ||
| // parser, so a config never passes Validate() only to fail at | ||
| // startup; | ||
| // - reverse: config's accepted set must match the parser's contract | ||
| // exactly, so a value added to the parser without updating config | ||
| // (which would spuriously reject a valid config) is caught too. | ||
|
|
||
| // TestChainsyncStrategyWhitelistParity checks config.AcceptedChainsyncStrategies | ||
| // against chainsync.ParseHeaderSyncStrategy. | ||
| func TestChainsyncStrategyWhitelistParity(t *testing.T) { | ||
| // canonical is the contract of chainsync.ParseHeaderSyncStrategy; | ||
| // it mirrors that function's switch. The set-equality check below | ||
| // ties config's list to it, and the parse check ties it to the real | ||
| // parser, so a change to the parser's switch fails this test until | ||
| // both this list and config's list are updated. | ||
| canonical := []string{ | ||
| "", "primary", "parallel", "round-robin", "roundrobin", "round_robin", | ||
| } | ||
| assertWhitelistParity( | ||
| t, | ||
| "chainsync.strategy", | ||
| config.AcceptedChainsyncStrategies, | ||
| canonical, | ||
| func(v string) error { | ||
| _, err := chainsync.ParseHeaderSyncStrategy(v) | ||
| return err | ||
| }, | ||
| ) | ||
|
Comment on lines
+26
to
+57
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the parity test and the referenced parsers/contracts.
git ls-files cmd/dingo | sed -n '1,120p'
printf '\n--- config_parity_test.go ---\n'
cat -n cmd/dingo/config_parity_test.go | sed -n '1,220p'
printf '\n--- parser definitions ---\n'
rg -n "ParseHeaderSyncStrategy|AcceptedChainsyncStrategies|AcceptedMithrilBackends|Parse.*Mithril" -S .Repository: blinklabs-io/dingo Length of output: 8149 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Map candidate files before reading implementation slices.
fd -a ".*(chainsync|mithril).*" .
fd -a "config.*\.go|.*parity.*\.go" cmd/dingoRepository: blinklabs-io/dingo Length of output: 1892 🏁 Script executed: #!/bin/bash
set -euo pipefail
cat -n cmd/dingo/config_parity_test.go | sed -n '1,220p'Repository: blinklabs-io/dingo Length of output: 5358 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf 'pwd=%s\n' "$PWD"
git ls-files cmd/dingo/config_parity_test.go
cat -n cmd/dingo/config_parity_test.go | sed -n '1,220p'Repository: blinklabs-io/dingo Length of output: 5413 🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n "TestChainsyncStrategyWhitelistParity|assertWhitelistParity|canonical := \[\]string" cmd/dingo/config_parity_test.goRepository: blinklabs-io/dingo Length of output: 547 Use a shared accepted-value table Both parity tests still depend on hand-maintained 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| // TestMithrilBackendWhitelistParity checks config.AcceptedMithrilBackends | ||
| // against resolveMithrilBackend. | ||
| func TestMithrilBackendWhitelistParity(t *testing.T) { | ||
| // canonical is the contract of resolveMithrilBackend: the empty | ||
| // string (which selects v2) plus the two backend constants it | ||
| // switches on. | ||
| canonical := []string{"", mithril.BackendV1, mithril.BackendV2} | ||
| assertWhitelistParity( | ||
| t, | ||
| "mithril.backend", | ||
| config.AcceptedMithrilBackends, | ||
| canonical, | ||
| func(v string) error { | ||
| _, err := resolveMithrilBackend(v) | ||
| return err | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| // assertWhitelistParity verifies that configList (the values | ||
| // internal/config accepts) and canonical (the downstream parser's | ||
| // contract) describe the same set, and that both are actually accepted | ||
| // by the real parser via accepts. | ||
| func assertWhitelistParity( | ||
| t *testing.T, | ||
| name string, | ||
| configList, canonical []string, | ||
| accepts func(string) error, | ||
| ) { | ||
| t.Helper() | ||
| // Forward: everything config accepts must parse. | ||
| for _, v := range configList { | ||
| if err := accepts(v); err != nil { | ||
| t.Errorf( | ||
| "%s: config accepts %q but the parser rejects it: %v", | ||
| name, v, err, | ||
| ) | ||
| } | ||
| } | ||
| // Reverse: every value in the parser's contract must actually parse | ||
| // (anchoring the contract to the real parser) and config's set must | ||
| // match the contract exactly. | ||
| for _, v := range canonical { | ||
| if err := accepts(v); err != nil { | ||
| t.Errorf( | ||
| "%s: canonical value %q is rejected by the parser; "+ | ||
| "update the canonical set: %v", | ||
| name, v, err, | ||
| ) | ||
| } | ||
| } | ||
| if got, want := toStringSet(configList), toStringSet(canonical); !maps.Equal( | ||
| got, | ||
| want, | ||
| ) { | ||
| t.Errorf( | ||
| "%s: config accepted set %v does not match parser contract %v", | ||
| name, configList, canonical, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| func toStringSet(values []string) map[string]struct{} { | ||
| set := make(map[string]struct{}, len(values)) | ||
| for _, v := range values { | ||
| set[v] = struct{}{} | ||
| } | ||
| return set | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.