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