github.com/ryanbennettvoid/go-swagger@v0.18.1-0.20190104015444-3854bbbe2392/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(todo, `Validate(`)
   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  
   205  /* Template initTxxx() to prepare and load a fixture:
   206  
   207  func initTxxx() {
   208  	// testing xxx.yaml with expand (--with-expand)
   209  	f := newModelFixture("xxx.yaml", "A test blg")
   210  
   211  	// makes a run with expandSpec=false (full flattening)
   212  	thisRun := f.AddRun(false)
   213  
   214  	// loads expectations for model abc
   215  	thisRun.AddExpectations("abc.go", []string{
   216  		`line {`,
   217  		`	more codegen  		`,
   218  		`}`,
   219  	},
   220  		// not expected
   221  		noLines,
   222  		// output in Log
   223  		noLines,
   224  		noLines)
   225  
   226  	// loads expectations for model abcDef
   227  	thisRun.AddExpectations("abc_def.go", []string{}, []string{}, noLines, noLines)
   228  }
   229  
   230  */
   231  
   232  func TestModelGenerateDefinition(t *testing.T) {
   233  	// exercise the top level model generation func
   234  	log.SetOutput(ioutil.Discard)
   235  	defer func() {
   236  		log.SetOutput(os.Stdout)
   237  	}()
   238  	fixtureSpec := "../fixtures/bugs/1487/fixture-is-nullable.yaml"
   239  	assert := assert.New(t)
   240  	gendir, erd := ioutil.TempDir(".", "model-test")
   241  	defer func() {
   242  		_ = os.RemoveAll(gendir)
   243  	}()
   244  	if assert.NoError(erd) {
   245  		opts := &GenOpts{}
   246  		opts.IncludeValidator = true
   247  		opts.IncludeModel = true
   248  		opts.ValidateSpec = false
   249  		opts.Spec = fixtureSpec
   250  		opts.ModelPackage = "models"
   251  		opts.Target = gendir
   252  		if err := opts.EnsureDefaults(); err != nil {
   253  			panic(err)
   254  		}
   255  		// sets gen options (e.g. flatten vs expand) - flatten is the default setting
   256  		opts.FlattenOpts.Minimal = false
   257  
   258  		err := GenerateDefinition([]string{"thingWithNullableDates"}, opts)
   259  		assert.NoErrorf(err, "Expected GenerateDefinition() to run without error")
   260  
   261  		err = GenerateDefinition(nil, opts)
   262  		assert.NoErrorf(err, "Expected GenerateDefinition() to run without error")
   263  
   264  		opts.TemplateDir = gendir
   265  		err = GenerateDefinition([]string{"thingWithNullableDates"}, opts)
   266  		assert.NoErrorf(err, "Expected GenerateDefinition() to run without error")
   267  
   268  		err = GenerateDefinition([]string{"thingWithNullableDates"}, nil)
   269  		assert.Errorf(err, "Expected GenerateDefinition() return an error when no option is passed")
   270  
   271  		opts.TemplateDir = "templates"
   272  		err = GenerateDefinition([]string{"thingWithNullableDates"}, opts)
   273  		assert.Errorf(err, "Expected GenerateDefinition() to croak about protected templates")
   274  
   275  		opts.TemplateDir = ""
   276  		err = GenerateDefinition([]string{"myAbsentDefinition"}, opts)
   277  		assert.Errorf(err, "Expected GenerateDefinition() to return an error when the model is not in spec")
   278  
   279  		opts.Spec = "pathToNowhere"
   280  		err = GenerateDefinition([]string{"thingWithNullableDates"}, opts)
   281  		assert.Errorf(err, "Expected GenerateDefinition() to return an error when the spec is not reachable")
   282  	}
   283  }
   284  
   285  func TestMoreModelValidations(t *testing.T) {
   286  	log.SetOutput(ioutil.Discard)
   287  	defer func() {
   288  		log.SetOutput(os.Stdout)
   289  	}()
   290  	continueOnErrors := false
   291  	initModelFixtures()
   292  
   293  	dassert := assert.New(t)
   294  
   295  	t.Logf("INFO: model specs tested: %d", len(testedModels))
   296  	for _, fixt := range testedModels {
   297  		fixture := fixt
   298  		if fixture.SpecFile == "" {
   299  			continue
   300  		}
   301  		fixtureSpec := fixture.SpecFile
   302  		runTitle := strings.Join([]string{"codegen", strings.TrimSuffix(path.Base(fixtureSpec), path.Ext(fixtureSpec))}, "-")
   303  		t.Run(runTitle, func(t *testing.T) {
   304  			// gave up with parallel testing: when $ref analysis is involved, it is not possible to to parallelize
   305  			//t.Parallel()
   306  			log.SetOutput(ioutil.Discard)
   307  			for _, fixtureRun := range fixture.Runs {
   308  				// workaround race condition with underlying pkg: go-openapi/spec works with a global cache
   309  				// which does not support concurrent use for different specs.
   310  				//modelTestMutex.Lock()
   311  				specDoc, err := loads.Spec(fixtureSpec)
   312  				if !dassert.NoErrorf(err, "unexpected failure loading spec %s: %v", fixtureSpec, err) {
   313  					//modelTestMutex.Unlock()
   314  					t.FailNow()
   315  					return
   316  				}
   317  				opts := fixtureRun.FixtureOpts
   318  				// this is the expanded or flattened spec
   319  				newSpecDoc, er0 := validateAndFlattenSpec(opts, specDoc)
   320  				if !dassert.NoErrorf(er0, "could not expand/flatten fixture %s: %v", fixtureSpec, er0) {
   321  					//modelTestMutex.Unlock()
   322  					t.FailNow()
   323  					return
   324  				}
   325  				//modelTestMutex.Unlock()
   326  				definitions := newSpecDoc.Spec().Definitions
   327  				for k, fixtureExpectations := range fixtureRun.Definitions {
   328  					// pick definition to test
   329  					var schema *spec.Schema
   330  					var definitionName string
   331  					for def, s := range definitions {
   332  						// please do not inject fixtures with case conflicts on defs...
   333  						// this one is just easier to retrieve model back from file names when capturing
   334  						// the generated code.
   335  						if strings.EqualFold(def, k) {
   336  							schema = &s
   337  							definitionName = def
   338  							break
   339  						}
   340  					}
   341  					if !dassert.NotNil(schema, "expected to find definition %q in model fixture %s", k, fixtureSpec) {
   342  						//modelTestMutex.Unlock()
   343  						t.FailNow()
   344  						return
   345  					}
   346  					checkDefinitionCodegen(t, definitionName, fixtureSpec, schema, newSpecDoc, opts, fixtureExpectations, continueOnErrors)
   347  				}
   348  			}
   349  		})
   350  	}
   351  }
   352  
   353  func checkContinue(t *testing.T, continueOnErrors bool) {
   354  	if continueOnErrors {
   355  		t.Fail()
   356  	} else {
   357  		t.FailNow()
   358  	}
   359  }
   360  
   361  func checkDefinitionCodegen(t *testing.T, definitionName, fixtureSpec string, schema *spec.Schema, specDoc *loads.Document, opts *GenOpts, fixtureExpectations *modelExpectations, continueOnErrors bool) {
   362  	// prepare assertions on log output (e.g. generation warnings)
   363  	var logCapture bytes.Buffer
   364  	var msg string
   365  	dassert := assert.New(t)
   366  	if len(fixtureExpectations.ExpectedLogs) > 0 || len(fixtureExpectations.NotExpectedLogs) > 0 {
   367  		// lock when capturing shared log resource (hopefully not for all testcases)
   368  		modelTestMutex.Lock()
   369  		log.SetOutput(&logCapture)
   370  	}
   371  
   372  	// generate the schema for this definition
   373  	genModel, er1 := makeGenDefinition(definitionName, "models", *schema, specDoc, opts)
   374  	if len(fixtureExpectations.ExpectedLogs) > 0 || len(fixtureExpectations.NotExpectedLogs) > 0 {
   375  		msg = logCapture.String()
   376  		log.SetOutput(ioutil.Discard)
   377  		modelTestMutex.Unlock()
   378  	}
   379  
   380  	if fixtureExpectations.ExpectFailure && !dassert.Errorf(er1, "Expected an error during generation of definition %q from spec fixture %s", definitionName, fixtureSpec) {
   381  		// expected an error here, and it has not happened
   382  		checkContinue(t, continueOnErrors)
   383  		return
   384  	}
   385  	if !dassert.NoErrorf(er1, "could not generate model definition %q from spec fixture %s: %v", definitionName, fixtureSpec, er1) {
   386  		// expected smooth generation
   387  		checkContinue(t, continueOnErrors)
   388  		return
   389  	}
   390  	if len(fixtureExpectations.ExpectedLogs) > 0 || len(fixtureExpectations.NotExpectedLogs) > 0 {
   391  		// assert logged output
   392  		for line, logLine := range fixtureExpectations.ExpectedLogs {
   393  			if !assertInCode(t, strings.TrimSpace(logLine), msg) {
   394  				t.Logf("log expected did not match for definition %q in fixture %s at (fixture) log line %d", definitionName, fixtureSpec, line)
   395  			}
   396  		}
   397  		for line, logLine := range fixtureExpectations.NotExpectedLogs {
   398  			if !assertNotInCode(t, strings.TrimSpace(logLine), msg) {
   399  				t.Logf("log unexpectedly matched for definition %q in fixture %s at (fixture) log line %d", definitionName, fixtureSpec, line)
   400  			}
   401  		}
   402  		if t.Failed() && !continueOnErrors {
   403  			t.FailNow()
   404  			return
   405  		}
   406  	}
   407  
   408  	// execute the model template with this schema
   409  	buf := bytes.NewBuffer(nil)
   410  	er2 := templates.MustGet("model").Execute(buf, genModel)
   411  	if !dassert.NoErrorf(er2, "could not render model template for definition %q in spec fixture %s: %v", definitionName, fixtureSpec, er2) {
   412  		checkContinue(t, continueOnErrors)
   413  		return
   414  	}
   415  	outputName := fixtureExpectations.GeneratedFile
   416  	if outputName == "" {
   417  		outputName = swag.ToFileName(definitionName) + ".go"
   418  	}
   419  
   420  	// run goimport, gofmt on the generated code
   421  	formatted, er3 := opts.LanguageOpts.FormatContent(outputName, buf.Bytes())
   422  	if !dassert.NoErrorf(er3, "could not render model template for definition %q in spec fixture %s: %v", definitionName, fixtureSpec, er2) {
   423  		checkContinue(t, continueOnErrors)
   424  		return
   425  	}
   426  
   427  	// asserts generated code (see fixture file)
   428  	res := string(formatted)
   429  	for line, codeLine := range fixtureExpectations.ExpectedLines {
   430  		if !assertInCode(t, strings.TrimSpace(codeLine), res) {
   431  			t.Logf("code expected did not match for definition %q in fixture %s at (fixture) line %d", definitionName, fixtureSpec, line)
   432  		}
   433  	}
   434  	for line, codeLine := range fixtureExpectations.NotExpectedLines {
   435  		if !assertNotInCode(t, strings.TrimSpace(codeLine), res) {
   436  			t.Logf("code unexpectedly matched for definition %q in fixture %s at (fixture) line %d", definitionName, fixtureSpec, line)
   437  		}
   438  	}
   439  	if t.Failed() && !continueOnErrors {
   440  		t.FailNow()
   441  		return
   442  	}
   443  }
   444  
   445  /*
   446  // Gave up with parallel testing
   447  // TestModelRace verifies how much of the load/expand/flatten process may be parallelized:
   448  // by placing proper locks, global cache pollution in go-openapi/spec may be avoided.
   449  func TestModelRace(t *testing.T) {
   450  	log.SetOutput(ioutil.Discard)
   451  	defer func() {
   452  		log.SetOutput(os.Stdout)
   453  	}()
   454  	initModelFixtures()
   455  
   456  	dassert := assert.New(t)
   457  
   458  	for i := 0; i < 10; i++ {
   459  		t.Logf("Iteration: %d", i)
   460  
   461  		for _, fixt := range testedModels {
   462  			fixture := fixt
   463  			if fixture.SpecFile == "" {
   464  				continue
   465  			}
   466  			fixtureSpec := fixture.SpecFile
   467  			runTitle := strings.Join([]string{"codegen", strings.TrimSuffix(path.Base(fixtureSpec), path.Ext(fixtureSpec))}, "-")
   468  			t.Run(runTitle, func(t *testing.T) {
   469  				t.Parallel()
   470  				log.SetOutput(ioutil.Discard)
   471  				for _, fixtureRun := range fixture.Runs {
   472  
   473  					// loads defines the start of the critical section because it comes with the global cache initialized
   474  					// TODO: should make this cache more manageable in go-openapi/spec
   475  					modelTestMutex.Lock()
   476  					specDoc, err := loads.Spec(fixtureSpec)
   477  					if !dassert.NoErrorf(err, "unexpected failure loading spec %s: %v", fixtureSpec, err) {
   478  						modelTestMutex.Unlock()
   479  						t.FailNow()
   480  						return
   481  					}
   482  					opts := fixtureRun.FixtureOpts
   483  					// this is the expanded or flattened spec
   484  					newSpecDoc, er0 := validateAndFlattenSpec(opts, specDoc)
   485  					if !dassert.NoErrorf(er0, "could not expand/flatten fixture %s: %v", fixtureSpec, er0) {
   486  						modelTestMutex.Unlock()
   487  						t.FailNow()
   488  						return
   489  					}
   490  					modelTestMutex.Unlock()
   491  					definitions := newSpecDoc.Spec().Definitions
   492  					for k := range fixtureRun.Definitions {
   493  						// pick definition to test
   494  						var schema *spec.Schema
   495  						for def, s := range definitions {
   496  							if strings.EqualFold(def, k) {
   497  								schema = &s
   498  								break
   499  							}
   500  						}
   501  						if !dassert.NotNil(schema, "expected to find definition %q in model fixture %s", k, fixtureSpec) {
   502  							t.FailNow()
   503  							return
   504  						}
   505  					}
   506  				}
   507  			})
   508  		}
   509  	}
   510  }
   511  */