github.com/emreu/go-swagger@v0.22.1/generator/moreschemavalidation_test.go (about)

     1  // Copyright 2015 go-swagger maintainers
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //    http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package generator
    16  
    17  import (
    18  	"bytes"
    19  	"io/ioutil"
    20  	"log"
    21  	"os"
    22  	"path"
    23  	"strings"
    24  	"sync"
    25  	"testing"
    26  
    27  	"github.com/go-openapi/loads"
    28  	"github.com/go-openapi/spec"
    29  	"github.com/go-openapi/swag"
    30  	"github.com/stretchr/testify/assert"
    31  )
    32  
    33  // modelExpectations is a test structure to capture expected codegen lines of code
    34  type modelExpectations struct {
    35  	GeneratedFile    string
    36  	ExpectedLines    []string
    37  	NotExpectedLines []string
    38  	ExpectedLogs     []string
    39  	NotExpectedLogs  []string
    40  	ExpectFailure    bool
    41  }
    42  
    43  // modelTestRun is a test structure to configure generations options to test a spec
    44  type modelTestRun struct {
    45  	FixtureOpts *GenOpts
    46  	Definitions map[string]*modelExpectations
    47  }
    48  
    49  // AddExpectations adds expected / not expected sets of lines of code to the current run
    50  func (r *modelTestRun) AddExpectations(file string, expectedCode, notExpectedCode, expectedLogs, notExpectedLogs []string) {
    51  	k := strings.ToLower(swag.ToJSONName(strings.TrimSuffix(file, ".go")))
    52  	if def, ok := r.Definitions[k]; ok {
    53  		def.ExpectedLines = append(def.ExpectedLines, expectedCode...)
    54  		def.NotExpectedLines = append(def.NotExpectedLines, notExpectedCode...)
    55  		def.ExpectedLogs = append(def.ExpectedLogs, expectedLogs...)
    56  		def.NotExpectedLogs = append(def.NotExpectedLogs, notExpectedLogs...)
    57  		return
    58  	}
    59  	r.Definitions[k] = &modelExpectations{
    60  		GeneratedFile:    file,
    61  		ExpectedLines:    expectedCode,
    62  		NotExpectedLines: notExpectedCode,
    63  		ExpectedLogs:     expectedLogs,
    64  		NotExpectedLogs:  notExpectedLogs,
    65  	}
    66  }
    67  
    68  // ExpectedFor returns the map of model expectations from the run for a given model definition
    69  func (r *modelTestRun) ExpectedFor(definition string) *modelExpectations {
    70  	if def, ok := r.Definitions[strings.ToLower(definition)]; ok {
    71  		return def
    72  	}
    73  	return nil
    74  }
    75  
    76  func (r *modelTestRun) WithMinimalFlatten(minimal bool) *modelTestRun {
    77  	r.FixtureOpts.FlattenOpts.Minimal = minimal
    78  	return r
    79  }
    80  
    81  // modelFixture is a test structure to launch configurable test runs on a given spec
    82  type modelFixture struct {
    83  	SpecFile    string
    84  	Description string
    85  	Runs        []*modelTestRun
    86  }
    87  
    88  // Add adds a new run to the provided model fixture
    89  func (f *modelFixture) AddRun(expandSpec bool) *modelTestRun {
    90  	opts := &GenOpts{}
    91  	opts.IncludeValidator = true
    92  	opts.IncludeModel = true
    93  	opts.ValidateSpec = false
    94  	opts.Spec = f.SpecFile
    95  	if err := opts.EnsureDefaults(); err != nil {
    96  		panic(err)
    97  	}
    98  
    99  	// sets gen options (e.g. flatten vs expand) - full flatten is the default setting for this test (NOT the default CLI option!)
   100  	opts.FlattenOpts.Expand = expandSpec
   101  	opts.FlattenOpts.Minimal = false
   102  
   103  	defs := make(map[string]*modelExpectations, 150)
   104  	run := &modelTestRun{
   105  		FixtureOpts: opts,
   106  		Definitions: defs,
   107  	}
   108  	f.Runs = append(f.Runs, run)
   109  	return run
   110  }
   111  
   112  // ExpectedBy returns the expectations from another run of the current fixture, recalled by its index in the list of planned runs
   113  func (f *modelFixture) ExpectedFor(index int, definition string) *modelExpectations {
   114  	if index > len(f.Runs)-1 {
   115  		return nil
   116  	}
   117  	if def, ok := f.Runs[index].Definitions[strings.ToLower(definition)]; ok {
   118  		return def
   119  	}
   120  	return nil
   121  }
   122  
   123  // newModelFixture is a test utility to build a new test plan for a spec file.
   124  // The returned structure may be then used to add runs and expectations to each run.
   125  func newModelFixture(specFile string, description string) *modelFixture {
   126  	// lookup if already here
   127  	for _, fix := range testedModels {
   128  		if fix.SpecFile == specFile {
   129  			return fix
   130  		}
   131  	}
   132  	runs := make([]*modelTestRun, 0, 2)
   133  	fix := &modelFixture{
   134  		SpecFile:    specFile,
   135  		Description: description,
   136  		Runs:        runs,
   137  	}
   138  	testedModels = append(testedModels, fix)
   139  	return fix
   140  }
   141  
   142  // all tested specs: init at the end of this source file
   143  // you may append to those with different initXXX() funcs below.
   144  var (
   145  	modelTestMutex = &sync.Mutex{}
   146  	testedModels   []*modelFixture
   147  
   148  	// convenient vars for (not) matching some lines
   149  	noLines     []string
   150  	todo        []string
   151  	validatable []string
   152  	warning     []string
   153  )
   154  
   155  func init() {
   156  	testedModels = make([]*modelFixture, 0, 50)
   157  	noLines = []string{}
   158  	todo = []string{`TODO`}
   159  	validatable = append([]string{`Validate(`}, todo...)
   160  	warning = []string{`warning`}
   161  }
   162  
   163  // initModelFixtures loads all tests to be performed
   164  func initModelFixtures() {
   165  	initFixtureSimpleAllOf()
   166  	initFixtureComplexAllOf()
   167  	initFixtureIsNullable()
   168  	initFixtureItching()
   169  	initFixtureAdditionalProps()
   170  	initFixtureTuple()
   171  	initFixture1479Part()
   172  	initFixture1198()
   173  	initFixture1042()
   174  	initFixture1042V2()
   175  	initFixture979()
   176  	initFixture842()
   177  	initFixture607()
   178  	initFixture1336()
   179  	initFixtureErrors()
   180  	initFixture844Variations()
   181  	initFixtureMoreAddProps()
   182  	// a more stringent verification of this known fixture
   183  	initTodolistSchemavalidation()
   184  	initFixture1537()
   185  	initFixture1537v2()
   186  
   187  	// more maps and nullability checks
   188  	initFixture15365()
   189  	initFixtureNestedMaps()
   190  	initFixtureDeepMaps()
   191  
   192  	// format "byte" validation
   193  	initFixture1548()
   194  
   195  	// more tuples
   196  	initFixtureSimpleTuple()
   197  
   198  	// allOf with properties
   199  	initFixture1617()
   200  
   201  	// type realiasing
   202  	initFixtureRealiasedTypes()
   203  
   204  	// required base type
   205  	initFixture1993()
   206  
   207  	// allOf marshallers
   208  	initFixture2071()
   209  }
   210  
   211  /* Template initTxxx() to prepare and load a fixture:
   212  
   213  func initTxxx() {
   214  	// testing xxx.yaml with expand (--with-expand)
   215  	f := newModelFixture("xxx.yaml", "A test blg")
   216  
   217  	// makes a run with expandSpec=false (full flattening)
   218  	thisRun := f.AddRun(false)
   219  
   220  	// loads expectations for model abc
   221  	thisRun.AddExpectations("abc.go", []string{
   222  		`line {`,
   223  		`	more codegen  		`,
   224  		`}`,
   225  	},
   226  		// not expected
   227  		noLines,
   228  		// output in Log
   229  		noLines,
   230  		noLines)
   231  
   232  	// loads expectations for model abcDef
   233  	thisRun.AddExpectations("abc_def.go", []string{}, []string{}, noLines, noLines)
   234  }
   235  
   236  */
   237  
   238  func TestModelGenerateDefinition(t *testing.T) {
   239  	// exercise the top level model generation func
   240  	log.SetOutput(ioutil.Discard)
   241  	defer func() {
   242  		log.SetOutput(os.Stdout)
   243  	}()
   244  	fixtureSpec := "../fixtures/bugs/1487/fixture-is-nullable.yaml"
   245  	assert := assert.New(t)
   246  	gendir, erd := ioutil.TempDir(".", "model-test")
   247  	defer func() {
   248  		_ = os.RemoveAll(gendir)
   249  	}()
   250  	if assert.NoError(erd) {
   251  		opts := &GenOpts{}
   252  		opts.IncludeValidator = true
   253  		opts.IncludeModel = true
   254  		opts.ValidateSpec = false
   255  		opts.Spec = fixtureSpec
   256  		opts.ModelPackage = "models"
   257  		opts.Target = gendir
   258  		if err := opts.EnsureDefaults(); err != nil {
   259  			panic(err)
   260  		}
   261  		// sets gen options (e.g. flatten vs expand) - flatten is the default setting
   262  		opts.FlattenOpts.Minimal = false
   263  
   264  		err := GenerateDefinition([]string{"thingWithNullableDates"}, opts)
   265  		assert.NoErrorf(err, "Expected GenerateDefinition() to run without error")
   266  
   267  		err = GenerateDefinition(nil, opts)
   268  		assert.NoErrorf(err, "Expected GenerateDefinition() to run without error")
   269  
   270  		opts.TemplateDir = gendir
   271  		err = GenerateDefinition([]string{"thingWithNullableDates"}, opts)
   272  		assert.NoErrorf(err, "Expected GenerateDefinition() to run without error")
   273  
   274  		err = GenerateDefinition([]string{"thingWithNullableDates"}, nil)
   275  		assert.Errorf(err, "Expected GenerateDefinition() return an error when no option is passed")
   276  
   277  		opts.TemplateDir = "templates"
   278  		err = GenerateDefinition([]string{"thingWithNullableDates"}, opts)
   279  		assert.Errorf(err, "Expected GenerateDefinition() to croak about protected templates")
   280  
   281  		opts.TemplateDir = ""
   282  		err = GenerateDefinition([]string{"myAbsentDefinition"}, opts)
   283  		assert.Errorf(err, "Expected GenerateDefinition() to return an error when the model is not in spec")
   284  
   285  		opts.Spec = "pathToNowhere"
   286  		err = GenerateDefinition([]string{"thingWithNullableDates"}, opts)
   287  		assert.Errorf(err, "Expected GenerateDefinition() to return an error when the spec is not reachable")
   288  	}
   289  }
   290  
   291  func TestMoreModelValidations(t *testing.T) {
   292  	log.SetOutput(ioutil.Discard)
   293  	defer func() {
   294  		log.SetOutput(os.Stdout)
   295  	}()
   296  	continueOnErrors := false
   297  	initModelFixtures()
   298  
   299  	dassert := assert.New(t)
   300  
   301  	t.Logf("INFO: model specs tested: %d", len(testedModels))
   302  	for _, fixt := range testedModels {
   303  		fixture := fixt
   304  		if fixture.SpecFile == "" {
   305  			continue
   306  		}
   307  		fixtureSpec := fixture.SpecFile
   308  		runTitle := strings.Join([]string{"codegen", strings.TrimSuffix(path.Base(fixtureSpec), path.Ext(fixtureSpec))}, "-")
   309  		t.Run(runTitle, func(t *testing.T) {
   310  			// gave up with parallel testing: when $ref analysis is involved, it is not possible to to parallelize
   311  			//t.Parallel()
   312  			log.SetOutput(ioutil.Discard)
   313  			for _, fixtureRun := range fixture.Runs {
   314  				// workaround race condition with underlying pkg: go-openapi/spec works with a global cache
   315  				// which does not support concurrent use for different specs.
   316  				//modelTestMutex.Lock()
   317  				specDoc, err := loads.Spec(fixtureSpec)
   318  				if !dassert.NoErrorf(err, "unexpected failure loading spec %s: %v", fixtureSpec, err) {
   319  					//modelTestMutex.Unlock()
   320  					t.FailNow()
   321  					return
   322  				}
   323  				opts := fixtureRun.FixtureOpts
   324  				// this is the expanded or flattened spec
   325  				newSpecDoc, er0 := validateAndFlattenSpec(opts, specDoc)
   326  				if !dassert.NoErrorf(er0, "could not expand/flatten fixture %s: %v", fixtureSpec, er0) {
   327  					//modelTestMutex.Unlock()
   328  					t.FailNow()
   329  					return
   330  				}
   331  				//modelTestMutex.Unlock()
   332  				definitions := newSpecDoc.Spec().Definitions
   333  				for k, fixtureExpectations := range fixtureRun.Definitions {
   334  					// pick definition to test
   335  					var schema *spec.Schema
   336  					var definitionName string
   337  					for def, s := range definitions {
   338  						// please do not inject fixtures with case conflicts on defs...
   339  						// this one is just easier to retrieve model back from file names when capturing
   340  						// the generated code.
   341  						if strings.EqualFold(def, k) {
   342  							schema = &s
   343  							definitionName = def
   344  							break
   345  						}
   346  					}
   347  					if !dassert.NotNil(schema, "expected to find definition %q in model fixture %s", k, fixtureSpec) {
   348  						//modelTestMutex.Unlock()
   349  						t.FailNow()
   350  						return
   351  					}
   352  					checkDefinitionCodegen(t, definitionName, fixtureSpec, schema, newSpecDoc, opts, fixtureExpectations, continueOnErrors)
   353  				}
   354  			}
   355  		})
   356  	}
   357  }
   358  
   359  func checkContinue(t *testing.T, continueOnErrors bool) {
   360  	if continueOnErrors {
   361  		t.Fail()
   362  	} else {
   363  		t.FailNow()
   364  	}
   365  }
   366  
   367  func checkDefinitionCodegen(t *testing.T, definitionName, fixtureSpec string, schema *spec.Schema, specDoc *loads.Document, opts *GenOpts, fixtureExpectations *modelExpectations, continueOnErrors bool) {
   368  	// prepare assertions on log output (e.g. generation warnings)
   369  	var logCapture bytes.Buffer
   370  	var msg string
   371  	dassert := assert.New(t)
   372  	if len(fixtureExpectations.ExpectedLogs) > 0 || len(fixtureExpectations.NotExpectedLogs) > 0 {
   373  		// lock when capturing shared log resource (hopefully not for all testcases)
   374  		modelTestMutex.Lock()
   375  		log.SetOutput(&logCapture)
   376  	}
   377  
   378  	// generate the schema for this definition
   379  	genModel, er1 := makeGenDefinition(definitionName, "models", *schema, specDoc, opts)
   380  	if len(fixtureExpectations.ExpectedLogs) > 0 || len(fixtureExpectations.NotExpectedLogs) > 0 {
   381  		msg = logCapture.String()
   382  		log.SetOutput(ioutil.Discard)
   383  		modelTestMutex.Unlock()
   384  	}
   385  
   386  	if fixtureExpectations.ExpectFailure && !dassert.Errorf(er1, "Expected an error during generation of definition %q from spec fixture %s", definitionName, fixtureSpec) {
   387  		// expected an error here, and it has not happened
   388  		checkContinue(t, continueOnErrors)
   389  		return
   390  	}
   391  	if !dassert.NoErrorf(er1, "could not generate model definition %q from spec fixture %s: %v", definitionName, fixtureSpec, er1) {
   392  		// expected smooth generation
   393  		checkContinue(t, continueOnErrors)
   394  		return
   395  	}
   396  	if len(fixtureExpectations.ExpectedLogs) > 0 || len(fixtureExpectations.NotExpectedLogs) > 0 {
   397  		// assert logged output
   398  		for line, logLine := range fixtureExpectations.ExpectedLogs {
   399  			if !assertInCode(t, strings.TrimSpace(logLine), msg) {
   400  				t.Logf("log expected did not match for definition %q in fixture %s at (fixture) log line %d", definitionName, fixtureSpec, line)
   401  			}
   402  		}
   403  		for line, logLine := range fixtureExpectations.NotExpectedLogs {
   404  			if !assertNotInCode(t, strings.TrimSpace(logLine), msg) {
   405  				t.Logf("log unexpectedly matched for definition %q in fixture %s at (fixture) log line %d", definitionName, fixtureSpec, line)
   406  			}
   407  		}
   408  		if t.Failed() && !continueOnErrors {
   409  			t.FailNow()
   410  			return
   411  		}
   412  	}
   413  
   414  	// execute the model template with this schema
   415  	buf := bytes.NewBuffer(nil)
   416  	er2 := templates.MustGet("model").Execute(buf, genModel)
   417  	if !dassert.NoErrorf(er2, "could not render model template for definition %q in spec fixture %s: %v", definitionName, fixtureSpec, er2) {
   418  		checkContinue(t, continueOnErrors)
   419  		return
   420  	}
   421  	outputName := fixtureExpectations.GeneratedFile
   422  	if outputName == "" {
   423  		outputName = swag.ToFileName(definitionName) + ".go"
   424  	}
   425  
   426  	// run goimport, gofmt on the generated code
   427  	formatted, er3 := opts.LanguageOpts.FormatContent(outputName, buf.Bytes())
   428  	if !dassert.NoErrorf(er3, "could not render model template for definition %q in spec fixture %s: %v", definitionName, fixtureSpec, er2) {
   429  		checkContinue(t, continueOnErrors)
   430  		return
   431  	}
   432  
   433  	// asserts generated code (see fixture file)
   434  	res := string(formatted)
   435  	for line, codeLine := range fixtureExpectations.ExpectedLines {
   436  		if !assertInCode(t, strings.TrimSpace(codeLine), res) {
   437  			t.Logf("code expected did not match for definition %q in fixture %s at (fixture) line %d", definitionName, fixtureSpec, line)
   438  		}
   439  	}
   440  	for line, codeLine := range fixtureExpectations.NotExpectedLines {
   441  		if !assertNotInCode(t, strings.TrimSpace(codeLine), res) {
   442  			t.Logf("code unexpectedly matched for definition %q in fixture %s at (fixture) line %d", definitionName, fixtureSpec, line)
   443  		}
   444  	}
   445  	if t.Failed() && !continueOnErrors {
   446  		t.FailNow()
   447  		return
   448  	}
   449  }
   450  
   451  /*
   452  // Gave up with parallel testing
   453  // TestModelRace verifies how much of the load/expand/flatten process may be parallelized:
   454  // by placing proper locks, global cache pollution in go-openapi/spec may be avoided.
   455  func TestModelRace(t *testing.T) {
   456  	log.SetOutput(ioutil.Discard)
   457  	defer func() {
   458  		log.SetOutput(os.Stdout)
   459  	}()
   460  	initModelFixtures()
   461  
   462  	dassert := assert.New(t)
   463  
   464  	for i := 0; i < 10; i++ {
   465  		t.Logf("Iteration: %d", i)
   466  
   467  		for _, fixt := range testedModels {
   468  			fixture := fixt
   469  			if fixture.SpecFile == "" {
   470  				continue
   471  			}
   472  			fixtureSpec := fixture.SpecFile
   473  			runTitle := strings.Join([]string{"codegen", strings.TrimSuffix(path.Base(fixtureSpec), path.Ext(fixtureSpec))}, "-")
   474  			t.Run(runTitle, func(t *testing.T) {
   475  				t.Parallel()
   476  				log.SetOutput(ioutil.Discard)
   477  				for _, fixtureRun := range fixture.Runs {
   478  
   479  					// loads defines the start of the critical section because it comes with the global cache initialized
   480  					// TODO: should make this cache more manageable in go-openapi/spec
   481  					modelTestMutex.Lock()
   482  					specDoc, err := loads.Spec(fixtureSpec)
   483  					if !dassert.NoErrorf(err, "unexpected failure loading spec %s: %v", fixtureSpec, err) {
   484  						modelTestMutex.Unlock()
   485  						t.FailNow()
   486  						return
   487  					}
   488  					opts := fixtureRun.FixtureOpts
   489  					// this is the expanded or flattened spec
   490  					newSpecDoc, er0 := validateAndFlattenSpec(opts, specDoc)
   491  					if !dassert.NoErrorf(er0, "could not expand/flatten fixture %s: %v", fixtureSpec, er0) {
   492  						modelTestMutex.Unlock()
   493  						t.FailNow()
   494  						return
   495  					}
   496  					modelTestMutex.Unlock()
   497  					definitions := newSpecDoc.Spec().Definitions
   498  					for k := range fixtureRun.Definitions {
   499  						// pick definition to test
   500  						var schema *spec.Schema
   501  						for def, s := range definitions {
   502  							if strings.EqualFold(def, k) {
   503  								schema = &s
   504  								break
   505  							}
   506  						}
   507  						if !dassert.NotNil(schema, "expected to find definition %q in model fixture %s", k, fixtureSpec) {
   508  							t.FailNow()
   509  							return
   510  						}
   511  					}
   512  				}
   513  			})
   514  		}
   515  	}
   516  }
   517  */