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