Files
dolt/go/libraries/utils/minver/minvertesting.go
T
Brian Hendriks b6cd726af3 move config
2024-05-15 10:27:16 -07:00

222 lines
5.3 KiB
Go

// Copyright 2024 Dolthub, Inc.
//
// 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 minver
import (
"errors"
"fmt"
"io"
"os"
"reflect"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/dolthub/dolt/go/libraries/utils/structwalk"
"github.com/dolthub/dolt/go/libraries/utils/version"
)
type FieldInfo struct {
Name string
TypeStr string
MinVer string
YamlTag string
}
func FieldInfoFromLine(l string) (FieldInfo, error) {
l = strings.TrimSpace(l)
tokens := strings.Split(l, " ")
if len(tokens) != 4 {
return FieldInfo{}, fmt.Errorf("invalid line in minver_validation.txt: '%s'", l)
}
return FieldInfo{
Name: tokens[0],
TypeStr: tokens[1],
MinVer: tokens[2],
YamlTag: tokens[3],
}, nil
}
func FieldInfoFromStructField(field reflect.StructField, depth int) FieldInfo {
info := FieldInfo{
Name: strings.Repeat("-", depth) + field.Name,
TypeStr: strings.Replace(field.Type.String(), " ", "", -1),
MinVer: field.Tag.Get("minver"),
YamlTag: field.Tag.Get("yaml"),
}
if info.MinVer == "" {
info.MinVer = "0.0.0"
}
return info
}
func (fi FieldInfo) Equals(other FieldInfo) bool {
return fi.Name == other.Name && fi.TypeStr == other.TypeStr && fi.MinVer == other.MinVer && fi.YamlTag == other.YamlTag
}
func (fi FieldInfo) String() string {
return fmt.Sprintf("%s %s %s %s", fi.Name, fi.TypeStr, fi.MinVer, fi.YamlTag)
}
type MinVerValidationReader struct {
lines []string
current int
}
func OpenMinVerValidation(path string) (*MinVerValidationReader, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
lines := strings.Split(string(data), "\n")
return &MinVerValidationReader{
lines: lines,
current: -1,
}, nil
}
func (r *MinVerValidationReader) Advance() {
for r.current < len(r.lines) {
r.current++
if r.current < len(r.lines) {
l := r.lines[r.current]
if !strings.HasPrefix(l, "#") {
return
}
}
}
}
func (r *MinVerValidationReader) Current() (FieldInfo, error) {
if r.current < 0 {
r.Advance()
}
if r.current < 0 || r.current < len(r.lines) {
l := r.lines[r.current]
return FieldInfoFromLine(l)
}
return FieldInfo{}, io.EOF
}
func ValidateMinVerFunc(field reflect.StructField, depth int) error {
var hasMinVer bool
var hasOmitEmpty bool
minVerTag := field.Tag.Get("minver")
if minVerTag != "" {
if minVerTag != "TBD" {
if _, err := version.Encode(minVerTag); err != nil {
return fmt.Errorf("invalid minver tag on field %s '%s': %w", field.Name, minVerTag, err)
}
}
hasMinVer = true
}
isNullable := field.Type.Kind() == reflect.Ptr || field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Map
if hasMinVer && !isNullable {
return fmt.Errorf("field '%s' has a version tag '%s' but is not nullable", field.Name, minVerTag)
}
yamlTag := field.Tag.Get("yaml")
if yamlTag == "" {
return fmt.Errorf("required tag 'yaml' missing on field '%s'", field.Name)
} else {
vals := strings.Split(yamlTag, ",")
for _, val := range vals {
if val == "omitempty" {
hasOmitEmpty = true
break
}
}
}
if hasMinVer && !hasOmitEmpty {
return fmt.Errorf("field '%s' has a version tag '%s' but no yaml tag with omitempty", field.Name, minVerTag)
}
return nil
}
func ValidateAgainstFile(t *testing.T, path string, st any) {
rd, err := OpenMinVerValidation(path)
require.NoError(t, err)
rd.Advance()
err = structwalk.Walk(st, func(field reflect.StructField, depth int) error {
fi := FieldInfoFromStructField(field, depth)
prevFI, err := rd.Current()
if err != nil && !errors.Is(err, io.EOF) {
return err
}
if prevFI.Equals(fi) {
rd.Advance()
return nil
}
if fi.MinVer == "TBD" {
return nil
}
if errors.Is(err, io.EOF) {
return fmt.Errorf("new field '%s' added", fi.String())
} else {
// You are seeing this error because a new config field was added that didn't meet the requirements.
// See the comment in "TestMinVer" which covers the requirements of new fields.
return fmt.Errorf("expected '%s' but got '%s'", prevFI.String(), fi.String())
}
})
require.NoError(t, err)
}
func GenValidationFile(st any, outFile string) error {
lines := []string{
"# file automatically updated by the release process.",
"# if you are getting an error with this file it's likely you",
"# have added a new minver tag with a value other than TBD",
}
err := structwalk.Walk(st, func(field reflect.StructField, depth int) error {
fi := FieldInfoFromStructField(field, depth)
lines = append(lines, fi.String())
return nil
})
if err != nil {
return fmt.Errorf("error generating data for '%s': %w", outFile, err)
}
fileContents := strings.Join(lines, "\n")
err = os.WriteFile(outFile, []byte(fileContents), 0644)
if err != nil {
return fmt.Errorf("error writing '%s': %w", outFile, err)
}
return nil
}