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 */