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