github.com/rzurga/go-swagger@v0.28.1-0.20211109195225-5d1f453ffa3a/hack/codegen_nonreg_test.go (about)

     1  //+build ignore
     2  
     3  package main
     4  
     5  import (
     6  	"flag"
     7  	"fmt"
     8  	"io/ioutil"
     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.v2"
    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 := ioutil.TempDir(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 := ioutil.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 := ioutil.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  }