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