github.com/google/go-github/v74@v74.0.0/github/gen-stringify-test.go (about) 1 // Copyright 2019 The go-github AUTHORS. All rights reserved. 2 // 3 // Use of this source code is governed by a BSD-style 4 // license that can be found in the LICENSE file. 5 6 //go:build ignore 7 8 // gen-stringify-test generates test methods to test the String methods. 9 // 10 // These tests eliminate most of the code coverage problems so that real 11 // code coverage issues can be more readily identified. 12 // 13 // It is meant to be used by go-github contributors in conjunction with the 14 // go generate tool before sending a PR to GitHub. 15 // Please see the CONTRIBUTING.md file for more information. 16 package main 17 18 import ( 19 "bytes" 20 "flag" 21 "fmt" 22 "go/ast" 23 "go/format" 24 "go/parser" 25 "go/token" 26 "log" 27 "os" 28 "strings" 29 "text/template" 30 ) 31 32 const ( 33 ignoreFilePrefix1 = "gen-" 34 ignoreFilePrefix2 = "github-" 35 outputFileSuffix = "-stringify_test.go" 36 ) 37 38 var ( 39 verbose = flag.Bool("v", false, "Print verbose log messages") 40 41 // skipStructMethods lists "struct.method" combos to skip. 42 skipStructMethods = map[string]bool{} 43 // skipStructs lists structs to skip. 44 skipStructs = map[string]bool{ 45 "RateLimits": true, 46 } 47 48 funcMap = template.FuncMap{ 49 "isNotLast": func(index int, slice []*structField) string { 50 if index+1 < len(slice) { 51 return ", " 52 } 53 return "" 54 }, 55 "processZeroValue": func(v string) string { 56 switch v { 57 case "Ptr(false)": 58 return "false" 59 case "Ptr(0.0)": 60 return "0" 61 case "0", "Ptr(0)", "Ptr(int64(0))": 62 return "0" 63 case `""`, `Ptr("")`: 64 return `""` 65 case "Timestamp{}", "&Timestamp{}": 66 return "github.Timestamp{0001-01-01 00:00:00 +0000 UTC}" 67 case "nil": 68 return "map[]" 69 case `[]int{0}`: 70 return `[0]` 71 case `[]string{""}`: 72 return `[""]` 73 case "[]Scope{ScopeNone}": 74 return `["(no scope)"]` 75 case "[]any{nil}": 76 return "[<nil>]" 77 } 78 log.Fatalf("Unhandled zero value: %q", v) 79 return "" 80 }, 81 } 82 83 sourceTmpl = template.Must(template.New("source").Funcs(funcMap).Parse(source)) 84 ) 85 86 func main() { 87 flag.Parse() 88 fset := token.NewFileSet() 89 90 pkgs, err := parser.ParseDir(fset, ".", sourceFilter, 0) 91 if err != nil { 92 log.Fatal(err) 93 return 94 } 95 96 for pkgName, pkg := range pkgs { 97 t := &templateData{ 98 filename: pkgName + outputFileSuffix, 99 Year: 2019, // No need to change this once set (even in following years). 100 Package: pkgName, 101 Imports: map[string]string{"testing": "testing"}, 102 StringFuncs: map[string]bool{}, 103 StructFields: map[string][]*structField{}, 104 } 105 for filename, f := range pkg.Files { 106 logf("Processing %v...", filename) 107 if err := t.processAST(f); err != nil { 108 log.Fatal(err) 109 } 110 } 111 if err := t.dump(); err != nil { 112 log.Fatal(err) 113 } 114 } 115 logf("Done.") 116 } 117 118 func sourceFilter(fi os.FileInfo) bool { 119 return !strings.HasSuffix(fi.Name(), "_test.go") && 120 !strings.HasPrefix(fi.Name(), ignoreFilePrefix1) && 121 !strings.HasPrefix(fi.Name(), ignoreFilePrefix2) 122 } 123 124 type templateData struct { 125 filename string 126 Year int 127 Package string 128 Imports map[string]string 129 StringFuncs map[string]bool 130 StructFields map[string][]*structField 131 } 132 133 type structField struct { 134 sortVal string // Lower-case version of "ReceiverType.FieldName". 135 ReceiverVar string // The one-letter variable name to match the ReceiverType. 136 ReceiverType string 137 FieldName string 138 FieldType string 139 ZeroValue string 140 NamedStruct bool // Getter for named struct. 141 } 142 143 func (t *templateData) processAST(f *ast.File) error { 144 for _, decl := range f.Decls { 145 fn, ok := decl.(*ast.FuncDecl) 146 if ok { 147 if fn.Recv != nil && len(fn.Recv.List) > 0 { 148 id, ok := fn.Recv.List[0].Type.(*ast.Ident) 149 if ok && fn.Name.Name == "String" { 150 logf("Got FuncDecl: Name=%q, id.Name=%#v", fn.Name.Name, id.Name) 151 t.StringFuncs[id.Name] = true 152 } else { 153 star, ok := fn.Recv.List[0].Type.(*ast.StarExpr) 154 if ok && fn.Name.Name == "String" { 155 id, ok := star.X.(*ast.Ident) 156 if ok { 157 logf("Got FuncDecl: Name=%q, id.Name=%#v", fn.Name.Name, id.Name) 158 t.StringFuncs[id.Name] = true 159 } else { 160 logf("Ignoring FuncDecl: Name=%q, Type=%T", fn.Name.Name, fn.Recv.List[0].Type) 161 } 162 } else { 163 logf("Ignoring FuncDecl: Name=%q, Type=%T", fn.Name.Name, fn.Recv.List[0].Type) 164 } 165 } 166 } else { 167 logf("Ignoring FuncDecl: Name=%q, fn=%#v", fn.Name.Name, fn) 168 } 169 continue 170 } 171 172 gd, ok := decl.(*ast.GenDecl) 173 if !ok { 174 logf("Ignoring AST decl type %T", decl) 175 continue 176 } 177 178 for _, spec := range gd.Specs { 179 ts, ok := spec.(*ast.TypeSpec) 180 if !ok { 181 continue 182 } 183 // Skip unexported identifiers. 184 if !ts.Name.IsExported() { 185 logf("Struct %v is unexported; skipping.", ts.Name) 186 continue 187 } 188 // Check if the struct should be skipped. 189 if skipStructs[ts.Name.Name] { 190 logf("Struct %v is in skip list; skipping.", ts.Name) 191 continue 192 } 193 st, ok := ts.Type.(*ast.StructType) 194 if !ok { 195 logf("Ignoring AST type %T, Name=%q", ts.Type, ts.Name.String()) 196 continue 197 } 198 for _, field := range st.Fields.List { 199 if len(field.Names) == 0 { 200 continue 201 } 202 203 fieldName := field.Names[0] 204 if id, ok := field.Type.(*ast.Ident); ok { 205 t.addIdent(id, ts.Name.String(), fieldName.String()) 206 continue 207 } 208 209 if at, ok := field.Type.(*ast.ArrayType); ok { 210 if id, ok := at.Elt.(*ast.Ident); ok { 211 t.addIdentSlice(id, ts.Name.String(), fieldName.String()) 212 continue 213 } 214 } 215 216 se, ok := field.Type.(*ast.StarExpr) 217 if !ok { 218 logf("Ignoring type %T for Name=%q, FieldName=%q", field.Type, ts.Name.String(), fieldName.String()) 219 continue 220 } 221 222 // Skip unexported identifiers. 223 if !fieldName.IsExported() { 224 logf("Field %v is unexported; skipping.", fieldName) 225 continue 226 } 227 // Check if "struct.method" should be skipped. 228 if key := fmt.Sprintf("%v.Get%v", ts.Name, fieldName); skipStructMethods[key] { 229 logf("Method %v is in skip list; skipping.", key) 230 continue 231 } 232 233 switch x := se.X.(type) { 234 case *ast.ArrayType: 235 case *ast.Ident: 236 t.addIdentPtr(x, ts.Name.String(), fieldName.String()) 237 case *ast.MapType: 238 case *ast.SelectorExpr: 239 default: 240 logf("processAST: type %q, field %q, unknown %T: %+v", ts.Name, fieldName, x, x) 241 } 242 } 243 } 244 } 245 return nil 246 } 247 248 func (t *templateData) addMapType(receiverType, fieldName string) { 249 t.StructFields[receiverType] = append(t.StructFields[receiverType], newStructField(receiverType, fieldName, "map[]", "nil", false)) 250 } 251 252 func (t *templateData) addIdent(x *ast.Ident, receiverType, fieldName string) { 253 var zeroValue string 254 var namedStruct = false 255 switch x.String() { 256 case "int": 257 zeroValue = "0" 258 case "int64": 259 zeroValue = "0" 260 case "float64": 261 zeroValue = "0.0" 262 case "string": 263 zeroValue = `""` 264 case "bool": 265 zeroValue = "false" 266 case "Timestamp": 267 zeroValue = "Timestamp{}" 268 default: 269 zeroValue = "nil" 270 namedStruct = true 271 } 272 273 t.StructFields[receiverType] = append(t.StructFields[receiverType], newStructField(receiverType, fieldName, x.String(), zeroValue, namedStruct)) 274 } 275 276 func (t *templateData) addIdentPtr(x *ast.Ident, receiverType, fieldName string) { 277 var zeroValue string 278 var namedStruct = false 279 switch x.String() { 280 case "int": 281 zeroValue = "Ptr(0)" 282 case "int64": 283 zeroValue = "Ptr(int64(0))" 284 case "float64": 285 zeroValue = "Ptr(0.0)" 286 case "string": 287 zeroValue = `Ptr("")` 288 case "bool": 289 zeroValue = "Ptr(false)" 290 case "Timestamp": 291 zeroValue = "&Timestamp{}" 292 default: 293 zeroValue = "nil" 294 namedStruct = true 295 } 296 297 t.StructFields[receiverType] = append(t.StructFields[receiverType], newStructField(receiverType, fieldName, x.String(), zeroValue, namedStruct)) 298 } 299 300 func (t *templateData) addIdentSlice(x *ast.Ident, receiverType, fieldName string) { 301 var zeroValue string 302 var namedStruct = false 303 switch x.String() { 304 case "int": 305 zeroValue = "[]int{0}" 306 case "int64": 307 zeroValue = "[]int64{0}" 308 case "float64": 309 zeroValue = "[]float64{0}" 310 case "string": 311 zeroValue = `[]string{""}` 312 case "bool": 313 zeroValue = "[]bool{false}" 314 case "Scope": 315 zeroValue = "[]Scope{ScopeNone}" 316 case "any": 317 zeroValue = "[]any{nil}" 318 // case "Timestamp": 319 // zeroValue = "&Timestamp{}" 320 default: 321 zeroValue = "nil" 322 namedStruct = true 323 } 324 325 t.StructFields[receiverType] = append(t.StructFields[receiverType], newStructField(receiverType, fieldName, x.String(), zeroValue, namedStruct)) 326 } 327 328 func (t *templateData) dump() error { 329 if len(t.StructFields) == 0 { 330 logf("No StructFields for %v; skipping.", t.filename) 331 return nil 332 } 333 334 // Remove unused structs. 335 var toDelete []string 336 for k := range t.StructFields { 337 if !t.StringFuncs[k] { 338 toDelete = append(toDelete, k) 339 continue 340 } 341 } 342 for _, k := range toDelete { 343 delete(t.StructFields, k) 344 } 345 346 var buf bytes.Buffer 347 if err := sourceTmpl.Execute(&buf, t); err != nil { 348 return err 349 } 350 clean, err := format.Source(buf.Bytes()) 351 if err != nil { 352 log.Printf("failed-to-format source:\n%v", buf.String()) 353 return err 354 } 355 356 logf("Writing %v...", t.filename) 357 if err := os.Chmod(t.filename, 0644); err != nil { 358 return fmt.Errorf("os.Chmod(%q, 0644): %v", t.filename, err) 359 } 360 361 if err := os.WriteFile(t.filename, clean, 0444); err != nil { 362 return err 363 } 364 365 if err := os.Chmod(t.filename, 0444); err != nil { 366 return fmt.Errorf("os.Chmod(%q, 0444): %v", t.filename, err) 367 } 368 369 return nil 370 } 371 372 func newStructField(receiverType, fieldName, fieldType, zeroValue string, namedStruct bool) *structField { 373 return &structField{ 374 sortVal: strings.ToLower(receiverType) + "." + strings.ToLower(fieldName), 375 ReceiverVar: strings.ToLower(receiverType[:1]), 376 ReceiverType: receiverType, 377 FieldName: fieldName, 378 FieldType: fieldType, 379 ZeroValue: zeroValue, 380 NamedStruct: namedStruct, 381 } 382 } 383 384 func logf(fmt string, args ...any) { 385 if *verbose { 386 log.Printf(fmt, args...) 387 } 388 } 389 390 const source = `// Copyright {{.Year}} The go-github AUTHORS. All rights reserved. 391 // 392 // Use of this source code is governed by a BSD-style 393 // license that can be found in the LICENSE file. 394 395 // Code generated by gen-stringify-tests; DO NOT EDIT. 396 // Instead, please run "go generate ./..." as described here: 397 // https://github.com/google/go-github/blob/master/CONTRIBUTING.md#submitting-a-patch 398 399 package {{ $package := .Package}}{{$package}} 400 {{with .Imports}} 401 import ( 402 {{- range . -}} 403 "{{.}}" 404 {{end -}} 405 ) 406 {{end}} 407 {{range $key, $value := .StructFields}} 408 func Test{{ $key }}_String(t *testing.T) { 409 t.Parallel() 410 v := {{ $key }}{ {{range .}}{{if .NamedStruct}} 411 {{ .FieldName }}: &{{ .FieldType }}{},{{else}} 412 {{ .FieldName }}: {{.ZeroValue}},{{end}}{{end}} 413 } 414 want := ` + "`" + `{{ $package }}.{{ $key }}{{ $slice := . }}{ 415 {{- range $ind, $val := .}}{{if .NamedStruct}}{{ .FieldName }}:{{ $package }}.{{ .FieldType }}{}{{else}}{{ .FieldName }}:{{ processZeroValue .ZeroValue }}{{end}}{{ isNotLast $ind $slice }}{{end}}}` + "`" + ` 416 if got := v.String(); got != want { 417 t.Errorf("{{ $key }}.String = %v, want %v", got, want) 418 } 419 } 420 {{end}} 421 `