github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/utils/minver/minvertesting.go (about)

     1  // Copyright 2024 Dolthub, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package minver
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"io"
    21  	"os"
    22  	"reflect"
    23  	"strings"
    24  	"testing"
    25  
    26  	"github.com/stretchr/testify/require"
    27  
    28  	"github.com/dolthub/dolt/go/libraries/utils/structwalk"
    29  	"github.com/dolthub/dolt/go/libraries/utils/version"
    30  )
    31  
    32  type FieldInfo struct {
    33  	Name    string
    34  	TypeStr string
    35  	MinVer  string
    36  	YamlTag string
    37  }
    38  
    39  func FieldInfoFromLine(l string) (FieldInfo, error) {
    40  	l = strings.TrimSpace(l)
    41  	tokens := strings.Split(l, " ")
    42  
    43  	if len(tokens) != 4 {
    44  		return FieldInfo{}, fmt.Errorf("invalid line in minver_validation.txt: '%s'", l)
    45  	}
    46  
    47  	return FieldInfo{
    48  		Name:    tokens[0],
    49  		TypeStr: tokens[1],
    50  		MinVer:  tokens[2],
    51  		YamlTag: tokens[3],
    52  	}, nil
    53  }
    54  
    55  func FieldInfoFromStructField(field reflect.StructField, depth int) FieldInfo {
    56  	info := FieldInfo{
    57  		Name:    strings.Repeat("-", depth) + field.Name,
    58  		TypeStr: strings.Replace(field.Type.String(), " ", "", -1),
    59  		MinVer:  field.Tag.Get("minver"),
    60  		YamlTag: field.Tag.Get("yaml"),
    61  	}
    62  
    63  	if info.MinVer == "" {
    64  		info.MinVer = "0.0.0"
    65  	}
    66  
    67  	return info
    68  }
    69  
    70  func (fi FieldInfo) Equals(other FieldInfo) bool {
    71  	return fi.Name == other.Name && fi.TypeStr == other.TypeStr && fi.MinVer == other.MinVer && fi.YamlTag == other.YamlTag
    72  }
    73  
    74  func (fi FieldInfo) String() string {
    75  	return fmt.Sprintf("%s %s %s %s", fi.Name, fi.TypeStr, fi.MinVer, fi.YamlTag)
    76  }
    77  
    78  type MinVerValidationReader struct {
    79  	lines   []string
    80  	current int
    81  }
    82  
    83  func OpenMinVerValidation(path string) (*MinVerValidationReader, error) {
    84  	data, err := os.ReadFile(path)
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  
    89  	lines := strings.Split(string(data), "\n")
    90  
    91  	return &MinVerValidationReader{
    92  		lines:   lines,
    93  		current: -1,
    94  	}, nil
    95  }
    96  
    97  func (r *MinVerValidationReader) Advance() {
    98  	for r.current < len(r.lines) {
    99  		r.current++
   100  
   101  		if r.current < len(r.lines) {
   102  			l := r.lines[r.current]
   103  
   104  			if !strings.HasPrefix(l, "#") {
   105  				return
   106  			}
   107  		}
   108  	}
   109  }
   110  
   111  func (r *MinVerValidationReader) Current() (FieldInfo, error) {
   112  	if r.current < 0 {
   113  		r.Advance()
   114  	}
   115  
   116  	if r.current < 0 || r.current < len(r.lines) {
   117  		l := r.lines[r.current]
   118  		return FieldInfoFromLine(l)
   119  	}
   120  
   121  	return FieldInfo{}, io.EOF
   122  }
   123  
   124  func ValidateMinVerFunc(field reflect.StructField, depth int) error {
   125  	var hasMinVer bool
   126  	var hasOmitEmpty bool
   127  
   128  	minVerTag := field.Tag.Get("minver")
   129  	if minVerTag != "" {
   130  		if minVerTag != "TBD" {
   131  			if _, err := version.Encode(minVerTag); err != nil {
   132  				return fmt.Errorf("invalid minver tag on field %s '%s': %w", field.Name, minVerTag, err)
   133  			}
   134  		}
   135  		hasMinVer = true
   136  	}
   137  
   138  	isNullable := field.Type.Kind() == reflect.Ptr || field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Map
   139  	if hasMinVer && !isNullable {
   140  		return fmt.Errorf("field '%s' has a version tag '%s' but is not nullable", field.Name, minVerTag)
   141  	}
   142  
   143  	yamlTag := field.Tag.Get("yaml")
   144  	if yamlTag == "" {
   145  		return fmt.Errorf("required tag 'yaml' missing on field '%s'", field.Name)
   146  	} else {
   147  		vals := strings.Split(yamlTag, ",")
   148  		for _, val := range vals {
   149  			if val == "omitempty" {
   150  				hasOmitEmpty = true
   151  				break
   152  			}
   153  		}
   154  	}
   155  
   156  	if hasMinVer && !hasOmitEmpty {
   157  		return fmt.Errorf("field '%s' has a version tag '%s' but no yaml tag with omitempty", field.Name, minVerTag)
   158  	}
   159  
   160  	return nil
   161  }
   162  
   163  func ValidateAgainstFile(t *testing.T, path string, st any) {
   164  	rd, err := OpenMinVerValidation(path)
   165  	require.NoError(t, err)
   166  
   167  	rd.Advance()
   168  
   169  	err = structwalk.Walk(st, func(field reflect.StructField, depth int) error {
   170  		fi := FieldInfoFromStructField(field, depth)
   171  		prevFI, err := rd.Current()
   172  		if err != nil && !errors.Is(err, io.EOF) {
   173  			return err
   174  		}
   175  
   176  		if prevFI.Equals(fi) {
   177  			rd.Advance()
   178  			return nil
   179  		}
   180  
   181  		if fi.MinVer == "TBD" {
   182  			return nil
   183  		}
   184  
   185  		if errors.Is(err, io.EOF) {
   186  			return fmt.Errorf("new field '%s' added", fi.String())
   187  		} else {
   188  			// You are seeing this error because a new config field was added that didn't meet the requirements.
   189  			// See the comment in "TestMinVer" which covers the requirements of new fields.
   190  			return fmt.Errorf("expected '%s' but got '%s'", prevFI.String(), fi.String())
   191  		}
   192  	})
   193  	require.NoError(t, err)
   194  }
   195  
   196  func GenValidationFile(st any, outFile string) error {
   197  	lines := []string{
   198  		"# file automatically updated by the release process.",
   199  		"# if you are getting an error with this file it's likely you",
   200  		"# have added a new minver tag with a value other than TBD",
   201  	}
   202  
   203  	err := structwalk.Walk(st, func(field reflect.StructField, depth int) error {
   204  		fi := FieldInfoFromStructField(field, depth)
   205  		lines = append(lines, fi.String())
   206  		return nil
   207  	})
   208  
   209  	if err != nil {
   210  		return fmt.Errorf("error generating data for '%s': %w", outFile, err)
   211  	}
   212  
   213  	fileContents := strings.Join(lines, "\n")
   214  
   215  	err = os.WriteFile(outFile, []byte(fileContents), 0644)
   216  	if err != nil {
   217  		return fmt.Errorf("error writing '%s': %w", outFile, err)
   218  	}
   219  
   220  	return nil
   221  }