github.com/ffalor/go-swagger@v0.0.0-20231011000038-9f25265ac351/hack/codegen_nonreg_test.go (about) 1 //go:build ignore 2 // +build ignore 3 4 package main 5 6 import ( 7 "flag" 8 "fmt" 9 "log" 10 "os" 11 "path/filepath" 12 "regexp" 13 "strings" 14 "testing" 15 "time" 16 17 //color "github.com/logrusorgru/aurora" 18 "github.com/stretchr/testify/assert" 19 "github.com/stretchr/testify/require" 20 "gopkg.in/yaml.v3" 21 "gotest.tools/icmd" 22 ) 23 24 const ( 25 defaultFixtureFile = "codegen-fixtures.yaml" 26 genDir = "./tmp-gen" 27 serverName = "nrcodegen" 28 29 // run options 30 31 FullFlatten = "--with-flatten=full" 32 MinimalFlatten = "--with-flatten=minimal" 33 Expand = "--with-flatten=expand" 34 SkipValidation = "--skip-validation" 35 ) 36 37 // skipT indicates known failures to skip in the test suite 38 type skipT struct { 39 // known failures to be skipped 40 KnownFailure bool `yaml:"knownFailure,omitempty"` 41 KnownValidationFailure bool `yaml:"knownValidationFailure,omitempty"` 42 KnownClientFailure bool `yaml:"knownClientFailure,omitempty"` 43 KnownServerFailure bool `yaml:"knownServerFailure,omitempty"` 44 KnownExpandFailure bool `yaml:"knownExpandFailure,omitempty"` 45 KnownFlattenMinimalFailure bool `yaml:"knownFlattenMinimalFailure,omitempty"` 46 47 SkipModel bool `yaml:"skipModel,omitempty"` 48 SkipExpand bool `yaml:"skipExpand,omitempty"` 49 50 // global skip settings 51 SkipClient bool `yaml:"skipClient,omitempty"` 52 SkipServer bool `yaml:"skipServer,omitempty"` 53 SkipFullFlatten bool `yaml:"skipFullFlatten,omitempty"` 54 SkipValidation bool `yaml:"skipValidation,omitempty"` 55 } 56 57 // fixtureT describe a spec and what _not_ to do with it 58 type fixtureT struct { 59 Dir string `yaml:"dir,omitempty"` 60 Spec string `yaml:"spec,omitempty"` 61 Skipped skipT `yaml:"skipped,omitempty"` 62 } 63 64 type fixturesT map[string]skipT 65 66 // Update a fixture with a file key 67 func (f fixturesT) Update(key string, in skipT) { 68 out, ok := f[key] 69 if !ok { 70 f[key] = in 71 return 72 } 73 if in.KnownFailure { 74 out.KnownFailure = true 75 } 76 if in.KnownValidationFailure { 77 out.KnownValidationFailure = true 78 } 79 if in.KnownClientFailure { 80 out.KnownClientFailure = true 81 } 82 if in.KnownServerFailure { 83 out.KnownServerFailure = true 84 } 85 if in.KnownExpandFailure { 86 out.KnownExpandFailure = true 87 } 88 if in.KnownFlattenMinimalFailure { 89 out.KnownFlattenMinimalFailure = true 90 } 91 if in.SkipModel { 92 out.SkipModel = true 93 } 94 if in.SkipExpand { 95 out.SkipExpand = true 96 } 97 f[key] = out 98 } 99 100 // runT describes a test run with given options and generation targets 101 type runT struct { 102 Name string 103 GenOpts []string 104 Target string 105 Skip bool 106 GenClient bool 107 GenServer bool 108 GenModel bool 109 } 110 111 func (r runT) Opts() []string { 112 return append(r.GenOpts, "--target", r.Target) 113 } 114 115 func getRepoPath(t *testing.T) string { 116 res := icmd.RunCommand("git", "rev-parse", "--show-toplevel") 117 require.Equal(t, 0, res.ExitCode) 118 pth := res.Stdout() 119 pth = strings.Replace(pth, "\n", "", -1) 120 require.NotEmpty(t, pth) 121 return pth 122 } 123 124 func measure(t *testing.T, started *time.Time, args ...string) *time.Time { 125 if started == nil { 126 s := time.Now() 127 return &s 128 } 129 info(t, "elapsed %v: %v", args, time.Since(*started).Truncate(time.Second)) 130 return nil 131 } 132 133 func gobuild(t *testing.T, runOpts ...icmd.CmdOp) { 134 started := measure(t, nil) 135 cmd := icmd.Command("go", "build") 136 res := icmd.RunCmd(cmd, runOpts...) 137 if res.ExitCode == 127 { 138 // assume a transient error (e.g. memory): retry 139 warn(t, "build failure, assuming transitory issue and retrying") 140 time.Sleep(2 * time.Second) 141 res = icmd.RunCmd(cmd, runOpts...) 142 } 143 if !assert.Equal(t, 0, res.ExitCode) { 144 failure(t, "go build failed") 145 t.Log(res.Stderr()) 146 t.FailNow() 147 return 148 } 149 good(t, "go build of generated code OK") 150 _ = measure(t, started, "go build") 151 } 152 153 func generateModel(t *testing.T, spec string, runOpts []icmd.CmdOp, opts ...string) { 154 started := measure(t, nil) 155 cmd := icmd.Command("swagger", append([]string{"generate", "model", "--spec", spec, "--quiet"}, opts...)...) 156 res := icmd.RunCmd(cmd, runOpts...) 157 if !assert.Equal(t, 0, res.ExitCode) { 158 failure(t, "model generation failed for %s", spec) 159 t.Log(res.Stderr()) 160 t.FailNow() 161 return 162 } 163 good(t, "model generation OK") 164 _ = measure(t, started, "generate model", spec) 165 } 166 167 func buildModel(t *testing.T, target string) { 168 gobuild(t, icmd.Dir(filepath.Join(target, "models"))) 169 } 170 171 func generateServer(t *testing.T, spec string, runOpts []icmd.CmdOp, opts ...string) { 172 started := measure(t, nil) 173 cmd := icmd.Command("swagger", append([]string{"generate", "server", "--spec", spec, "--name", serverName, "--quiet"}, opts...)...) 174 res := icmd.RunCmd(cmd, runOpts...) 175 if !assert.Equal(t, 0, res.ExitCode) { 176 failure(t, "server generation failed for %s", spec) 177 t.Log(res.Stderr()) 178 t.FailNow() 179 return 180 } 181 good(t, "server generation OK") 182 _ = measure(t, started, "generate server", spec) 183 } 184 185 func buildServer(t *testing.T, target string) { 186 gobuild(t, icmd.Dir(filepath.Join(target, "cmd", serverName+"-server"))) 187 } 188 189 func generateClient(t *testing.T, spec string, runOpts []icmd.CmdOp, opts ...string) { 190 started := measure(t, nil) 191 cmd := icmd.Command("swagger", append([]string{"generate", "client", "--spec", spec, "--name", serverName, "--quiet"}, opts...)...) 192 res := icmd.RunCmd(cmd, runOpts...) 193 if !assert.Equal(t, 0, res.ExitCode) { 194 failure(t, "client generation failed for %s", spec) 195 t.Log(res.Stderr()) 196 t.FailNow() 197 return 198 } 199 good(t, "client generation OK") 200 _ = measure(t, started, "generate client", spec) 201 } 202 203 func buildClient(t *testing.T, target string) { 204 gobuild(t, icmd.Dir(filepath.Join(target, "client"))) 205 } 206 207 func warn(t *testing.T, msg string, args ...interface{}) { 208 //t.Log(color.Yellow(fmt.Sprintf(msg, args...))) 209 t.Log(fmt.Sprintf("WARN: "+msg, args...)) 210 } 211 212 func failure(t *testing.T, msg string, args ...interface{}) { 213 //t.Log(color.Red(fmt.Sprintf(msg, args...))) 214 t.Log(fmt.Sprintf("ERROR: "+msg, args...)) 215 } 216 217 func info(t *testing.T, msg string, args ...interface{}) { 218 //t.Log(color.Blue(fmt.Sprintf(msg, args...))) 219 t.Log(fmt.Sprintf("INFO: "+msg, args...)) 220 } 221 222 func good(t *testing.T, msg string, args ...interface{}) { 223 //t.Log(color.Green(fmt.Sprintf(msg, args...))) 224 t.Log(fmt.Sprintf("SUCCESS: "+msg, args...)) 225 } 226 227 func buildFixtures(t *testing.T, fixtures []fixtureT) fixturesT { 228 specMap := make(fixturesT, 200) 229 for _, fixture := range fixtures { 230 switch { 231 case fixture.Dir != "" && fixture.Spec == "": // get a directory of specs 232 for _, pattern := range []string{"*.yaml", "*.json", "*.yml"} { 233 specs, err := filepath.Glob(filepath.Join(filepath.FromSlash(fixture.Dir), pattern)) 234 require.NoErrorf(t, err, "could not match specs in %s", fixture.Dir) 235 for _, spec := range specs { 236 specMap.Update(spec, fixture.Skipped) 237 } 238 } 239 240 case fixture.Dir != "" && fixture.Spec != "": // get a specific spec 241 specMap.Update(filepath.Join(fixture.Dir, fixture.Spec), fixture.Skipped) 242 243 case fixture.Dir == "" && fixture.Spec != "": // enrich a specific spec with some skip descriptor 244 for _, pattern := range []string{"*", "*/*"} { 245 specs, err := filepath.Glob(filepath.Join("fixtures", pattern, fixture.Spec)) 246 require.NoErrorf(t, err, "could not match spec %s in fixtures", fixture.Spec) 247 for _, spec := range specs { 248 specMap.Update(spec, fixture.Skipped) 249 } 250 } 251 252 default: 253 failure(t, "invalid spec configuration: %v", fixture) 254 t.FailNow() 255 } 256 } 257 return specMap 258 } 259 260 func makeBuildDir(t *testing.T, spec string) string { 261 name := filepath.Base(spec) 262 parts := strings.Split(name, ".") 263 base := parts[0] 264 target, err := os.MkdirTemp(genDir, "gen-"+base+"-") 265 if err != nil { 266 failure(t, "cannot create temporary codegen dir for %s", base) 267 t.FailNow() 268 } 269 return target 270 } 271 272 // buildRuns determines generation options and targets, depending on known failures to skip. 273 func buildRuns(t *testing.T, spec string, skip, globalOpts skipT) []runT { 274 runs := make([]runT, 0, 10) 275 276 template := runT{ 277 GenOpts: make([]string, 0, 10), 278 GenClient: !globalOpts.SkipClient, 279 GenServer: !globalOpts.SkipServer, 280 GenModel: !globalOpts.SkipModel && !skip.SkipModel, 281 } 282 283 if skip.KnownFailure { 284 warn(t, "known failure: all generations skipped for %s", spec) 285 return []runT{{Skip: true}} 286 } 287 288 if skip.KnownValidationFailure || globalOpts.SkipValidation { 289 if skip.KnownValidationFailure { 290 info(t, "running without prior spec validation. Spec is formally invalid but generation may proceed for %s", spec) 291 } 292 template.GenOpts = append(template.GenOpts, SkipValidation) 293 } 294 295 if skip.KnownClientFailure { 296 warn(t, "known client generation failure: skipped for %s", spec) 297 template.GenClient = false 298 } 299 300 if skip.KnownServerFailure { 301 warn(t, "known server generation failure: skipped for %s", spec) 302 template.GenServer = false 303 } 304 305 if !skip.KnownExpandFailure && !globalOpts.SkipExpand && !skip.SkipExpand { 306 // safeguard: avoid discriminator use case for expand 307 doc, err := os.ReadFile(spec) 308 if err == nil && !strings.Contains(string(doc), "discriminator") { 309 expandRun := template 310 expandRun.Name = "expand spec run" 311 expandRun.GenOpts = append(expandRun.GenOpts, Expand) 312 expandRun.Target = makeBuildDir(t, spec) 313 runs = append(runs, expandRun) 314 } else if err == nil { 315 warn(t, "known failure with expand run (spec contains discriminator): skipped for %s", spec) 316 } 317 } else if skip.KnownExpandFailure { 318 warn(t, "known failure with expand run: skipped for %s", spec) 319 } 320 321 if !skip.KnownFlattenMinimalFailure { 322 flattenMinimalRun := template 323 flattenMinimalRun.Name = "minimal flatten spec run" 324 flattenMinimalRun.GenOpts = append(flattenMinimalRun.GenOpts, MinimalFlatten) 325 flattenMinimalRun.Target = makeBuildDir(t, spec) 326 runs = append(runs, flattenMinimalRun) 327 } else { 328 warn(t, "known failure with --flatten=minimal: skipped for %s and force --flatten=full", spec) 329 } 330 331 if !globalOpts.SkipFullFlatten || skip.KnownFlattenMinimalFailure { 332 flattenFulllRun := template 333 flattenFulllRun.Name = "full flatten spec run" 334 flattenFulllRun.GenOpts = append(flattenFulllRun.GenOpts, FullFlatten) 335 flattenFulllRun.Target = makeBuildDir(t, spec) 336 runs = append(runs, flattenFulllRun) 337 } 338 339 return runs 340 } 341 342 var ( 343 args struct { 344 skipModels bool 345 skipClients bool 346 skipServers bool 347 skipFlatten bool 348 skipExpand bool 349 fixtureFile string 350 runPattern string 351 excludePattern string 352 } 353 ) 354 355 func TestMain(m *testing.M) { 356 flag.BoolVar(&args.skipModels, "skip-models", false, "skips standalone model generation") 357 flag.BoolVar(&args.skipClients, "skip-clients", false, "skips client generation") 358 flag.BoolVar(&args.skipServers, "skip-servers", false, "skips server generation") 359 flag.BoolVar(&args.skipFlatten, "skip-full-flatten", false, "skips full flatten option from codegen runs") 360 flag.BoolVar(&args.skipExpand, "skip-expand", false, "skips spec expand option from codegen runs") 361 flag.StringVar(&args.fixtureFile, "fixture-file", defaultFixtureFile, "fixture configuration file") 362 flag.StringVar(&args.runPattern, "run", "", "regexp to include fixture") 363 flag.StringVar(&args.excludePattern, "exclude", "", "regexp to exclude fixture") 364 flag.Parse() 365 status := m.Run() 366 if status == 0 { 367 _ = os.RemoveAll(genDir) 368 //log.Println(color.Green("end of codegen runs. OK")) 369 log.Println("SUCCESS: end of codegen runs. OK") 370 } 371 os.Exit(status) 372 } 373 374 func loadFixtures(t *testing.T, in string) []fixtureT { 375 doc, err := os.ReadFile(in) 376 require.NoError(t, err) 377 fixtures := make([]fixtureT, 0, 200) 378 err = yaml.Unmarshal(doc, &fixtures) 379 require.NoError(t, err) 380 return fixtures 381 } 382 383 // TestCodegen runs codegen plan based for configured specifications 384 func TestCodegen(t *testing.T) { 385 repoPath := getRepoPath(t) 386 387 if args.fixtureFile == "" { 388 args.fixtureFile = defaultFixtureFile 389 } 390 391 fixtures := loadFixtures(t, args.fixtureFile) 392 393 err := os.Chdir(repoPath) 394 require.NoError(t, err) 395 396 _ = os.RemoveAll(genDir) 397 398 err = os.MkdirAll(genDir, os.ModeDir|os.ModePerm) 399 require.NoError(t, err) 400 info(t, "target generation in %s", genDir) 401 402 globalOpts := skipT{ 403 SkipFullFlatten: args.skipFlatten, 404 SkipExpand: args.skipExpand, 405 SkipModel: args.skipModels, 406 SkipClient: args.skipClients, 407 SkipServer: args.skipServers, 408 } 409 410 specMap := buildFixtures(t, fixtures) 411 cmdOpts := []icmd.CmdOp{icmd.Dir(repoPath)} 412 413 info(t, "running codegen for %d specs", len(specMap)) 414 415 if globalOpts.SkipClient { 416 info(t, "configured to skip client generations") 417 } 418 if globalOpts.SkipServer { 419 info(t, "configured to skip server generations") 420 } 421 if globalOpts.SkipModel { 422 info(t, "configured to skip model generation") 423 } 424 if globalOpts.SkipFullFlatten { 425 info(t, "configured to skip full flatten mode from generation runs") 426 } 427 if globalOpts.SkipExpand { 428 info(t, "configured to skip expand mode from generation runs") 429 } 430 431 for key, value := range specMap { 432 spec := key 433 skip := value 434 if args.runPattern != "" { 435 // include filter on a spec name pattern 436 re, err := regexp.Compile(args.runPattern) 437 require.NoError(t, err) 438 if !re.MatchString(spec) { 439 continue 440 } 441 } 442 if args.excludePattern != "" { 443 // exclude filter on a spec name pattern 444 re, err := regexp.Compile(args.excludePattern) 445 require.NoError(t, err) 446 if re.MatchString(spec) { 447 continue 448 } 449 } 450 t.Run(spec, func(t *testing.T) { 451 t.Parallel() 452 info(t, "codegen for spec %s", spec) 453 runs := buildRuns(t, spec, skip, globalOpts) 454 455 for _, toPin2 := range runs { 456 run := toPin2 457 if run.Skip { 458 warn(t, "%s: not tested against full build because of known codegen issues", spec) 459 continue 460 } 461 t.Run(run.Name, func(t *testing.T) { 462 t.Parallel() 463 if !run.GenClient && !skip.SkipClient || !run.GenModel && !skip.SkipModel || !run.GenServer && !skip.SkipServer { 464 info(t, "%s: some generations skipped ", spec) 465 } 466 467 info(t, "run %s for %s", run.Name, spec) 468 469 if run.GenModel { 470 generateModel(t, spec, cmdOpts, run.Opts()...) 471 buildModel(t, run.Target) 472 } 473 if run.GenServer { 474 generateServer(t, spec, cmdOpts, run.Opts()...) 475 buildServer(t, run.Target) 476 } 477 if run.GenClient { 478 generateClient(t, spec, cmdOpts, run.Opts()...) 479 buildClient(t, run.Target) 480 } 481 }) 482 } 483 }) 484 } 485 }