github.com/Percona-Lab/go-swagger@v0.19.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 // 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 */