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 }