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