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