talos/pkg/machinery/config/encoder/encoder_test.go
Dmitriy Matrenichev dad9c40c73
chore: simplify code
- 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>
2024-07-08 18:14:00 +03:00

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{})
}