mirror of
https://github.com/siderolabs/talos.git
synced 2026-04-18 04:02:02 +02:00
- replace `interface{}` with `any` using `gofmt -r 'interface{} -> any -w'`
- replace `a = []T{}` with `var a []T` where possible.
- replace `a = []T{}` with `a = make([]T, 0, len(b))` where possible.
Signed-off-by: Dmitriy Matrenichev <dmitry.matrenichev@siderolabs.com>
559 lines
11 KiB
Go
559 lines
11 KiB
Go
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
package encoder_test
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/suite"
|
|
yaml "gopkg.in/yaml.v3"
|
|
|
|
"github.com/siderolabs/talos/pkg/machinery/config/encoder"
|
|
)
|
|
|
|
type Config struct {
|
|
Integer int `yaml:"integer"`
|
|
Slice []string `yaml:"slice"`
|
|
ComplexSlice []*Endpoint `yaml:"complex_slice"`
|
|
Map map[string]*Endpoint `yaml:"map"`
|
|
|
|
Skip string `yaml:"-"`
|
|
Omit int `yaml:",omitempty"`
|
|
Inline *Mixin `yaml:",inline"`
|
|
|
|
CustomMarshaller *WithCustomMarshaller `yaml:",omitempty"`
|
|
Bytes []byte `yaml:"bytes,flow,omitempty"`
|
|
|
|
NilSlice Manifests `yaml:"nilslice,omitempty" talos:"omitonlyifnil"`
|
|
|
|
unexported int
|
|
}
|
|
|
|
type FakeConfig struct {
|
|
Machine Machine `yaml:"machine,omitempty"`
|
|
}
|
|
|
|
type Mixin struct {
|
|
MixedIn string `yaml:"mixed_in"`
|
|
}
|
|
|
|
type Endpoint struct {
|
|
Host string
|
|
Port int `yaml:",omitempty"`
|
|
}
|
|
|
|
type Machine struct {
|
|
State int
|
|
Config *MachineConfig `yaml:",omitempty"`
|
|
}
|
|
|
|
type MachineConfig struct {
|
|
Version string
|
|
Capabilities []string
|
|
}
|
|
|
|
type Manifests []Manifest
|
|
|
|
type Manifest struct {
|
|
Name string `yaml:"name"`
|
|
}
|
|
|
|
type WithCustomMarshaller struct {
|
|
value string
|
|
}
|
|
|
|
// MarshalYAML implements custom marshaller.
|
|
func (cm *WithCustomMarshaller) MarshalYAML() (any, error) {
|
|
node := &yaml.Node{}
|
|
|
|
if err := node.Encode(map[string]string{"value": cm.value}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
node.HeadComment = "completely custom"
|
|
|
|
return node, nil
|
|
}
|
|
|
|
// This is manually defined documentation data for Config.
|
|
// It is intended to be generated by `docgen` command.
|
|
var (
|
|
configDoc encoder.Doc
|
|
endpointDoc encoder.Doc
|
|
mixinDoc encoder.Doc
|
|
machineDoc encoder.Doc
|
|
machineConfigDoc encoder.Doc
|
|
)
|
|
|
|
func init() {
|
|
configDoc.Comments[encoder.LineComment] = "test configuration"
|
|
configDoc.Fields = make([]encoder.Doc, 11)
|
|
configDoc.Fields[1].Comments[encoder.LineComment] = "<<<"
|
|
configDoc.Fields[2].Comments[encoder.HeadComment] = "complex slice"
|
|
configDoc.Fields[3].Comments[encoder.FootComment] = "some text example for map"
|
|
|
|
configDoc.Fields[2].AddExample("slice example", []*Endpoint{{
|
|
Host: "127.0.0.1",
|
|
Port: 5554,
|
|
}})
|
|
|
|
configDoc.Fields[9].Comments[encoder.LineComment] = "A nilslice field is really cool."
|
|
configDoc.Fields[9].AddExample("nilslice example", Manifests{{
|
|
Name: "foo",
|
|
}})
|
|
|
|
endpointDoc.Comments[encoder.LineComment] = "endpoint settings"
|
|
endpointDoc.Fields = make([]encoder.Doc, 2)
|
|
endpointDoc.Fields[0].Comments[encoder.LineComment] = "endpoint host"
|
|
endpointDoc.Fields[1].Comments[encoder.LineComment] = "custom port"
|
|
|
|
mixinDoc.Fields = make([]encoder.Doc, 1)
|
|
mixinDoc.Fields[0].Comments[encoder.LineComment] = "was inlined"
|
|
|
|
machineDoc.AddExample("uncomment me", &Machine{
|
|
State: 100,
|
|
})
|
|
machineDoc.AddExample("second example", &Machine{
|
|
State: -1,
|
|
})
|
|
|
|
machineDoc.Fields = make([]encoder.Doc, 2)
|
|
machineDoc.Fields[1].AddExample("", &MachineConfig{
|
|
Version: "0.0.2",
|
|
})
|
|
|
|
machineConfigDoc.Fields = make([]encoder.Doc, 2)
|
|
machineConfigDoc.Fields[0].Comments[encoder.HeadComment] = "this is some version"
|
|
machineConfigDoc.Fields[1].AddExample("",
|
|
[]string{
|
|
"reboot", "upgrade",
|
|
},
|
|
)
|
|
}
|
|
|
|
func (c Config) Doc() *encoder.Doc {
|
|
return &configDoc
|
|
}
|
|
|
|
func (c Endpoint) Doc() *encoder.Doc {
|
|
return &endpointDoc
|
|
}
|
|
|
|
func (c Mixin) Doc() *encoder.Doc {
|
|
return &mixinDoc
|
|
}
|
|
|
|
func (c Machine) Doc() *encoder.Doc {
|
|
return &machineDoc
|
|
}
|
|
|
|
func (c MachineConfig) Doc() *encoder.Doc {
|
|
return &machineConfigDoc
|
|
}
|
|
|
|
// tests
|
|
|
|
type EncoderSuite struct {
|
|
suite.Suite
|
|
}
|
|
|
|
func (suite *EncoderSuite) TestRun() {
|
|
e := &Endpoint{
|
|
Port: 8080,
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
value any
|
|
expectedYAML string
|
|
incompatible bool
|
|
options []encoder.Option
|
|
}{
|
|
{
|
|
name: "default struct with all enabled",
|
|
value: &Config{},
|
|
expectedYAML: `integer: 0
|
|
# <<<
|
|
slice: []
|
|
# complex slice
|
|
complex_slice: []
|
|
# # slice example
|
|
# - host: 127.0.0.1 # endpoint host
|
|
# port: 5554 # custom port
|
|
|
|
map: {}
|
|
# some text example for map
|
|
# # A nilslice field is really cool.
|
|
|
|
# # nilslice example
|
|
# nilslice:
|
|
# - name: foo
|
|
`,
|
|
options: []encoder.Option{
|
|
encoder.WithComments(encoder.CommentsAll),
|
|
},
|
|
incompatible: true,
|
|
},
|
|
{
|
|
name: "default struct only with examples",
|
|
value: &Config{},
|
|
expectedYAML: `integer: 0
|
|
slice: []
|
|
complex_slice: []
|
|
# # slice example
|
|
# - host: 127.0.0.1
|
|
# port: 5554
|
|
|
|
map: {}
|
|
|
|
# # nilslice example
|
|
# nilslice:
|
|
# - name: foo
|
|
`,
|
|
options: []encoder.Option{
|
|
encoder.WithComments(encoder.CommentsExamples),
|
|
},
|
|
incompatible: true,
|
|
},
|
|
{
|
|
name: "default struct",
|
|
value: &Config{},
|
|
expectedYAML: `integer: 0
|
|
# <<<
|
|
slice: []
|
|
# complex slice
|
|
complex_slice: []
|
|
map: {}
|
|
# some text example for map
|
|
`,
|
|
options: []encoder.Option{
|
|
encoder.WithComments(encoder.CommentsDocs),
|
|
},
|
|
},
|
|
{
|
|
name: "struct with custom marshaller",
|
|
value: &Config{
|
|
CustomMarshaller: &WithCustomMarshaller{
|
|
value: "abcd",
|
|
},
|
|
},
|
|
expectedYAML: `integer: 0
|
|
# <<<
|
|
slice: []
|
|
# complex slice
|
|
complex_slice: []
|
|
map: {}
|
|
# some text example for map
|
|
|
|
custommarshaller:
|
|
# completely custom
|
|
value: abcd
|
|
`,
|
|
options: []encoder.Option{
|
|
encoder.WithComments(encoder.CommentsDocs),
|
|
},
|
|
},
|
|
{
|
|
name: "bytes flow",
|
|
value: &Config{
|
|
Bytes: []byte("..."),
|
|
},
|
|
expectedYAML: `integer: 0
|
|
# <<<
|
|
slice: []
|
|
# complex slice
|
|
complex_slice: []
|
|
map: {}
|
|
# some text example for map
|
|
|
|
bytes: [46, 46, 46]
|
|
`,
|
|
options: []encoder.Option{
|
|
encoder.WithComments(encoder.CommentsDocs),
|
|
},
|
|
},
|
|
{
|
|
name: "map check",
|
|
value: &Config{
|
|
Map: map[string]*Endpoint{
|
|
"endpoint": new(Endpoint),
|
|
},
|
|
unexported: -1,
|
|
},
|
|
expectedYAML: `integer: 0
|
|
# <<<
|
|
slice: []
|
|
# complex slice
|
|
complex_slice: []
|
|
map:
|
|
endpoint:
|
|
host: "" # endpoint host
|
|
# some text example for map
|
|
`,
|
|
options: []encoder.Option{
|
|
encoder.WithComments(encoder.CommentsDocs),
|
|
},
|
|
},
|
|
{
|
|
name: "nil map element",
|
|
value: &Config{
|
|
Map: map[string]*Endpoint{
|
|
"endpoint": nil,
|
|
},
|
|
},
|
|
expectedYAML: `integer: 0
|
|
# <<<
|
|
slice: []
|
|
# complex slice
|
|
complex_slice: []
|
|
map:
|
|
endpoint: null
|
|
# some text example for map
|
|
`,
|
|
options: []encoder.Option{
|
|
encoder.WithComments(encoder.CommentsDocs),
|
|
},
|
|
},
|
|
{
|
|
name: "nil map element",
|
|
value: &Config{
|
|
Map: map[string]*Endpoint{
|
|
"endpoint": new(Endpoint),
|
|
},
|
|
ComplexSlice: []*Endpoint{e},
|
|
},
|
|
expectedYAML: `integer: 0
|
|
# <<<
|
|
slice: []
|
|
# complex slice
|
|
complex_slice:
|
|
- host: "" # endpoint host
|
|
port: 8080 # custom port
|
|
map:
|
|
endpoint:
|
|
host: "" # endpoint host
|
|
# some text example for map
|
|
`,
|
|
options: []encoder.Option{
|
|
encoder.WithComments(encoder.CommentsDocs),
|
|
},
|
|
},
|
|
{
|
|
name: "inline",
|
|
value: &Config{
|
|
Inline: &Mixin{
|
|
MixedIn: "a",
|
|
},
|
|
},
|
|
expectedYAML: `integer: 0
|
|
# <<<
|
|
slice: []
|
|
# complex slice
|
|
complex_slice: []
|
|
map: {}
|
|
# some text example for map
|
|
|
|
mixed_in: a # was inlined
|
|
`,
|
|
options: []encoder.Option{
|
|
encoder.WithComments(encoder.CommentsDocs),
|
|
},
|
|
},
|
|
{
|
|
name: "comment example if zero",
|
|
value: &FakeConfig{},
|
|
expectedYAML: `# # uncomment me
|
|
# machine:
|
|
# state: 100
|
|
# config:
|
|
# # this is some version
|
|
# version: 0.0.2
|
|
# capabilities:
|
|
# - reboot
|
|
# - upgrade
|
|
# # second example
|
|
# machine:
|
|
# state: -1
|
|
# config:
|
|
# # this is some version
|
|
# version: 0.0.2
|
|
# capabilities:
|
|
# - reboot
|
|
# - upgrade
|
|
`,
|
|
incompatible: true,
|
|
},
|
|
{
|
|
name: "comment example if partially set",
|
|
value: &FakeConfig{
|
|
Machine{
|
|
State: 1000,
|
|
},
|
|
},
|
|
expectedYAML: `machine:
|
|
state: 1000
|
|
` + `
|
|
# config:
|
|
# # this is some version
|
|
# version: 0.0.2
|
|
# capabilities:
|
|
# - reboot
|
|
# - upgrade
|
|
`,
|
|
incompatible: true,
|
|
},
|
|
{
|
|
name: "populate map element's examples",
|
|
value: map[string][]*MachineConfig{
|
|
"first": {
|
|
{},
|
|
},
|
|
},
|
|
expectedYAML: `first:
|
|
- # this is some version
|
|
version: ""
|
|
capabilities: []
|
|
# - reboot
|
|
# - upgrade
|
|
`,
|
|
incompatible: true,
|
|
},
|
|
{
|
|
name: "without comments",
|
|
value: &FakeConfig{
|
|
Machine{
|
|
State: 1000,
|
|
},
|
|
},
|
|
expectedYAML: `machine:
|
|
state: 1000
|
|
`,
|
|
incompatible: true,
|
|
options: []encoder.Option{
|
|
encoder.WithComments(encoder.CommentsDisabled),
|
|
},
|
|
},
|
|
{
|
|
name: "only with docs",
|
|
value: &FakeConfig{},
|
|
expectedYAML: `{}
|
|
`,
|
|
incompatible: true,
|
|
options: []encoder.Option{
|
|
encoder.WithComments(encoder.CommentsDocs),
|
|
},
|
|
},
|
|
{
|
|
name: "only with examples",
|
|
value: &FakeConfig{},
|
|
expectedYAML: `# # uncomment me
|
|
# machine:
|
|
# state: 100
|
|
# config:
|
|
# version: 0.0.2
|
|
# capabilities:
|
|
# - reboot
|
|
# - upgrade
|
|
# # second example
|
|
# machine:
|
|
# state: -1
|
|
# config:
|
|
# version: 0.0.2
|
|
# capabilities:
|
|
# - reboot
|
|
# - upgrade
|
|
`,
|
|
incompatible: true,
|
|
options: []encoder.Option{
|
|
encoder.WithComments(encoder.CommentsExamples),
|
|
},
|
|
},
|
|
{
|
|
name: "with onlyifnotnil tag",
|
|
value: &Config{
|
|
NilSlice: Manifests{},
|
|
},
|
|
expectedYAML: `integer: 0
|
|
# <<<
|
|
slice: []
|
|
# complex slice
|
|
complex_slice: []
|
|
# # slice example
|
|
# - host: 127.0.0.1 # endpoint host
|
|
# port: 5554 # custom port
|
|
|
|
map: {}
|
|
# some text example for map
|
|
|
|
# A nilslice field is really cool.
|
|
nilslice: []
|
|
# # nilslice example
|
|
# - name: foo
|
|
`,
|
|
incompatible: true,
|
|
options: []encoder.Option{
|
|
encoder.WithComments(encoder.CommentsAll),
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
encoder := encoder.NewEncoder(test.value, test.options...)
|
|
data, err := encoder.Encode()
|
|
suite.Assert().NoError(err)
|
|
|
|
// compare with expected string output
|
|
suite.Assert().EqualValues(test.expectedYAML, string(data), test.name)
|
|
|
|
// decode into raw map to strip all comments
|
|
actualMap, err := decodeToMap(data)
|
|
suite.Assert().NoError(err)
|
|
|
|
// skip if marshaller output is not the same for our encoder and vanilla one
|
|
// note: it is only incompatible if config contains nested structs stored as value
|
|
// and if these nested structs are documented and you try to load generated yaml into map[interface{}]interface{}
|
|
if !test.incompatible {
|
|
// compare with regular yaml.Marshal call
|
|
expected, err := yaml.Marshal(test.value)
|
|
suite.Assert().NoError(err)
|
|
|
|
expectedMap, err := decodeToMap(expected)
|
|
suite.Assert().NoError(err)
|
|
suite.Assert().EqualValues(expectedMap, actualMap)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (suite *EncoderSuite) TestConcurrent() {
|
|
value := &Machine{}
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
for range 10 {
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
encoder := encoder.NewEncoder(value)
|
|
_, err := encoder.Encode()
|
|
suite.Assert().NoError(err)
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func decodeToMap(data []byte) (map[any]any, error) {
|
|
raw := map[any]any{}
|
|
err := yaml.Unmarshal(data, &raw)
|
|
|
|
return raw, err
|
|
}
|
|
|
|
func TestEncoderSuite(t *testing.T) {
|
|
suite.Run(t, &EncoderSuite{})
|
|
}
|