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  }