github.com/m3db/m3@v1.5.0/src/x/serialize/decoder_lifecycle_prop_test.go (about) 1 // Copyright (c) 2018 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package serialize 22 23 import ( 24 "bytes" 25 "fmt" 26 "math/rand" 27 "os" 28 "testing" 29 "time" 30 31 "github.com/m3db/m3/src/x/checked" 32 "github.com/m3db/m3/src/x/ident" 33 34 "github.com/leanovate/gopter" 35 "github.com/leanovate/gopter/commands" 36 "github.com/leanovate/gopter/gen" 37 ) 38 39 // NB(prateek): this file uses a SUT prop test to ensure we are correctly reference counting 40 // decoders including all interactions in their lifecycle. We use the structs below to model 41 // the system under test, and the valid state. 42 43 // multiDecoderSystem models the system under test. `primary` is the first decoder under test, 44 // `duplicates` are created by execution of the Duplicate() command, and we swap primary and 45 // duplicate upon execution of the Swap command. All methods executed upon the system (except) 46 // swap, are executed on the primary decoder, and subsequently invariants are checked upon 47 // the entire system (not just the primary). This combined with Swap() semantic, tests all 48 // decoders in the system. 49 type multiDecoderSystem struct { 50 primary TagDecoder 51 duplicates []TagDecoder 52 } 53 54 // multiDecoderState models the state of the system under test. It contains a mix of properties 55 // global to the system under test, and properties per decoder in the system. It follows the pattern 56 // used in multiDecoderSystem to have a single primary, and remaining duplicates. 57 type multiDecoderState struct { 58 // global properties of system under test 59 tags ident.Tags 60 initBytes checked.Bytes 61 numRefs int 62 // properties per decoder 63 primary decoderState 64 duplicates []decoderState 65 } 66 67 // decoderState models the state of a single decoder in the system. 68 type decoderState struct { 69 numTags int 70 numNextCalls int 71 closed bool 72 } 73 74 // systemAndResult is used to bypass a restriction in the gopter API 75 // which restricts the PostConditionFunc to operate upon nextState, and 76 // the result of executing a method on the system; to check the invariants 77 // we need, we have to query the state of the system under test too. To do so, 78 // we hijack the API by returning this struct, which contains both the system 79 // under test, and the result of an interaction as the "commands.Result". 80 type systemAndResult struct { 81 system *multiDecoderSystem 82 result commands.Result 83 } 84 85 func TestDecoderLifecycle(t *testing.T) { 86 parameters := gopter.DefaultTestParameters() 87 seed := time.Now().UnixNano() 88 parameters.MinSuccessfulTests = 100 89 parameters.MaxSize = 40 90 parameters.Rng = rand.New(rand.NewSource(seed)) 91 properties := gopter.NewProperties(parameters) 92 comms := decoderCommandsFunctor(t) 93 properties.Property("Decoder Lifecycle Invariants", commands.Prop(comms)) 94 reporter := gopter.NewFormatedReporter(true, 160, os.Stdout) 95 if !properties.Run(reporter) { 96 t.Errorf("failed with initial seed: %d", seed) 97 } 98 } 99 100 var decoderCommandsFunctor = func(t *testing.T) *commands.ProtoCommands { 101 return &commands.ProtoCommands{ 102 NewSystemUnderTestFunc: func(initialState commands.State) commands.SystemUnderTest { 103 sut := initialState.(*multiDecoderState) 104 d := newTestTagDecoder() 105 d.Reset(sut.initBytes) 106 if err := d.Err(); err != nil { 107 panic(err) 108 } 109 return &multiDecoderSystem{ 110 primary: d, 111 } 112 }, 113 DestroySystemUnderTestFunc: func(s commands.SystemUnderTest) { 114 sys := s.(*multiDecoderSystem) 115 sys.primary.Close() 116 for _, dupe := range sys.duplicates { 117 dupe.Close() 118 } 119 }, 120 InitialStateGen: newDecoderState(), 121 InitialPreConditionFunc: func(s commands.State) bool { 122 return s != nil 123 }, 124 GenCommandFunc: func(state commands.State) gopter.Gen { 125 return gen.OneGenOf( 126 gen.Const(nextCmd), 127 gen.Const(remainingCmd), 128 gen.Const(currentCmd), 129 gen.Const(closeCmd), 130 gen.Const(errCmd), 131 gen.Const(duplicateCmd), 132 gen.Const(swapToDuplicateCmd), 133 ) 134 }, 135 } 136 } 137 138 var errCmd = &commands.ProtoCommand{ 139 Name: "Err", 140 RunFunc: func(s commands.SystemUnderTest) commands.Result { 141 sys := s.(*multiDecoderSystem) 142 d := sys.primary 143 return &systemAndResult{ 144 system: sys, 145 result: d.Err(), 146 } 147 }, 148 PostConditionFunc: func(state commands.State, result commands.Result) *gopter.PropResult { 149 res := result.(*systemAndResult) 150 if res.result == nil { 151 return &gopter.PropResult{Status: gopter.PropTrue} 152 } 153 err := res.result.(error) 154 return &gopter.PropResult{ 155 Status: gopter.PropError, 156 Error: fmt.Errorf("received error [ err = %v, state = [%s] ]", err, state.(decoderState)), 157 } 158 }, 159 } 160 161 var currentCmd = &commands.ProtoCommand{ 162 Name: "Current", 163 RunFunc: func(s commands.SystemUnderTest) commands.Result { 164 sys := s.(*multiDecoderSystem) 165 d := sys.primary 166 return &systemAndResult{ 167 system: sys, 168 result: d.Current(), 169 } 170 }, 171 PostConditionFunc: func(state commands.State, result commands.Result) *gopter.PropResult { 172 decState := state.(*multiDecoderState) 173 res := result.(*systemAndResult).result.(ident.Tag) 174 if !decState.primary.hasCurrentTagsReference() { 175 if res.Name.Bytes() != nil || res.Value.Bytes() != nil { 176 return &gopter.PropResult{ 177 Status: gopter.PropError, 178 Error: fmt.Errorf("received not nil tags for closed state [ tag = %+v, state = [%s] ]", 179 res, decState), 180 } 181 } 182 // i.e. tag is nil and primary state should not have tags, all good. 183 return &gopter.PropResult{Status: gopter.PropTrue} 184 } 185 observedTag := res 186 expectedTag := decState.tags.Values()[decState.primary.numNextCalls-1] 187 if !observedTag.Name.Equal(expectedTag.Name) || 188 !observedTag.Value.Equal(expectedTag.Value) { 189 return &gopter.PropResult{ 190 Status: gopter.PropError, 191 Error: fmt.Errorf("unexpected tag received [ expected = %+v, observed = %+v, state = %s ]", 192 expectedTag, observedTag, decState), 193 } 194 } 195 // all good tags are equal 196 return &gopter.PropResult{Status: gopter.PropTrue} 197 }, 198 } 199 200 var nextCmd = &commands.ProtoCommand{ 201 Name: "Next", 202 RunFunc: func(s commands.SystemUnderTest) commands.Result { 203 sys := s.(*multiDecoderSystem) 204 d := sys.primary 205 return &systemAndResult{ 206 system: sys, 207 result: d.Next(), 208 } 209 }, 210 NextStateFunc: func(state commands.State) commands.State { 211 s := state.(*multiDecoderState) 212 if s.primary.closed { 213 return s 214 } 215 s.primary.numNextCalls++ 216 if s.primary.numTags <= 0 { 217 return s 218 } 219 if s.primary.numNextCalls == 1 { 220 // i.e. only increment tag references the first time we allocate 221 s.numRefs += 2 // tagName & tagValue 222 } 223 // when we have gone past the end, remove references to tagName/tagValue 224 if s.primary.numNextCalls == 1+s.primary.numTags { 225 s.numRefs -= 2 226 } 227 return s 228 }, 229 PostConditionFunc: func(state commands.State, result commands.Result) *gopter.PropResult { 230 res := result.(*systemAndResult) 231 decState := state.(*multiDecoderState) 232 if decState.primary.numRemaining() > 0 && !res.result.(bool) { 233 // ensure we were told the correct value for Next() 234 return &gopter.PropResult{ 235 Status: gopter.PropError, 236 Error: fmt.Errorf("received invalid Next()"), 237 } 238 } 239 // ensure hold the correct number of references for underlying bytes 240 sys := result.(*systemAndResult).system 241 return validateNumReferences(decState, sys) 242 }, 243 } 244 245 var remainingCmd = &commands.ProtoCommand{ 246 Name: "Remaining", 247 RunFunc: func(s commands.SystemUnderTest) commands.Result { 248 sys := s.(*multiDecoderSystem) 249 d := sys.primary 250 return &systemAndResult{ 251 system: sys, 252 result: d.Remaining(), 253 } 254 }, 255 PostConditionFunc: func(state commands.State, result commands.Result) *gopter.PropResult { 256 decState := state.(*multiDecoderState) 257 remain := result.(*systemAndResult).result.(int) 258 if remain != decState.primary.numRemaining() { 259 return &gopter.PropResult{ 260 Status: gopter.PropError, 261 Error: fmt.Errorf("received invalid Remain [ expected=%d, observed=%d ]", 262 decState.primary.numRemaining(), remain), 263 } 264 } 265 return &gopter.PropResult{Status: gopter.PropTrue} 266 }, 267 } 268 269 var duplicateCmd = &commands.ProtoCommand{ 270 Name: "Duplicate", 271 RunFunc: func(s commands.SystemUnderTest) commands.Result { 272 sys := s.(*multiDecoderSystem) 273 d := sys.primary 274 dupe := d.Duplicate().(TagDecoder) 275 sys.duplicates = append(sys.duplicates, dupe) 276 return &systemAndResult{ 277 system: sys, 278 } 279 }, 280 NextStateFunc: func(state commands.State) commands.State { 281 s := state.(*multiDecoderState) 282 if s.primary.closed { 283 s.duplicates = append(s.duplicates, s.primary) 284 return s 285 } 286 if !s.primary.closed { 287 // i.e. we have a checked bytes still present, so we 288 // atleast make another reference to it. 289 s.numRefs++ 290 } 291 // if we have any current tags, we should inc ref by 2 because of it 292 if s.primary.hasCurrentTagsReference() { 293 s.numRefs += 2 294 } 295 s.duplicates = append(s.duplicates, s.primary) 296 return s 297 }, 298 PostConditionFunc: func(state commands.State, result commands.Result) *gopter.PropResult { 299 sys := result.(*systemAndResult).system 300 decState := state.(*multiDecoderState) 301 return validateNumReferences(decState, sys) 302 }, 303 } 304 305 var closeCmd = &commands.ProtoCommand{ 306 Name: "Close", 307 RunFunc: func(s commands.SystemUnderTest) commands.Result { 308 sys := s.(*multiDecoderSystem) 309 d := sys.primary.(*decoder) 310 d.Close() 311 return &systemAndResult{ 312 system: sys, 313 } 314 }, 315 NextStateFunc: func(state commands.State) commands.State { 316 s := state.(*multiDecoderState) 317 if s.primary.closed { 318 return s 319 } 320 // drop primary reference 321 s.numRefs-- 322 // if we have any current tags, we should dec ref by 2 because of it 323 if s.primary.hasCurrentTagsReference() { 324 s.numRefs -= 2 325 } 326 s.primary.closed = true 327 return s 328 }, 329 PostConditionFunc: func(state commands.State, result commands.Result) *gopter.PropResult { 330 sys := result.(*systemAndResult).system 331 decState := state.(*multiDecoderState) 332 return validateNumReferences(decState, sys) 333 }, 334 } 335 336 // swapToDuplicate swaps the current system under test to be operating on the most recent Duplicate 337 // we created (if any). 338 var swapToDuplicateCmd = &commands.ProtoCommand{ 339 Name: "swapToDuplicate", 340 PreConditionFunc: func(s commands.State) bool { 341 state := s.(*multiDecoderState) 342 return len(state.duplicates) > 0 343 }, 344 NextStateFunc: func(state commands.State) commands.State { 345 s := state.(*multiDecoderState) 346 x, y := s.primary, s.duplicates[len(s.duplicates)-1] 347 s.duplicates[len(s.duplicates)-1] = x 348 s.primary = y 349 return s 350 }, 351 RunFunc: func(sys commands.SystemUnderTest) commands.Result { 352 s := sys.(*multiDecoderSystem) 353 x, y := s.primary, s.duplicates[len(s.duplicates)-1] 354 s.duplicates[len(s.duplicates)-1] = x 355 s.primary = y 356 return &systemAndResult{ 357 system: s, 358 } 359 }, 360 PostConditionFunc: func(state commands.State, result commands.Result) *gopter.PropResult { 361 sys := result.(*systemAndResult).system 362 decState := state.(*multiDecoderState) 363 return validateNumReferences(decState, sys) 364 }, 365 } 366 367 func validateNumReferences(decState *multiDecoderState, sys *multiDecoderSystem) *gopter.PropResult { 368 // ensure at least one decoder in the system have has a reference if we are not closed 369 if decState.numRefs != 0 { 370 found := false 371 if d := sys.primary.(*decoder).checkedData; d != nil { 372 found = true 373 } 374 for _, dupe := range sys.duplicates { 375 dec := dupe.(*decoder) 376 if d := dec.checkedData; d != nil { 377 found = true 378 } 379 } 380 if !found { 381 return &gopter.PropResult{Status: gopter.PropError, 382 Error: fmt.Errorf("expected at least one reference, observed all nil, state = %s", decState), 383 } 384 } 385 } 386 // ensure we hold the correct number of references for underlying bytes in all decoders 387 validate := func(dec *decoder, state decoderState, numRefs int) error { 388 // if decoder is closed, we should hold no references 389 if state.closed { 390 if dec.checkedData == nil { 391 return nil 392 } 393 if dec.checkedData != nil { 394 return fmt.Errorf("expected nil, observed %p references in [state = %s]", dec.checkedData, decState) 395 } 396 } 397 // i.e. decoder is not closed, so we should have a reference 398 if dec.checkedData == nil && numRefs != 0 { 399 return fmt.Errorf("expected %d num ref, observed nil in [state = %s]", numRefs, decState) 400 } 401 if dec.checkedData.NumRef() != numRefs { 402 return fmt.Errorf("expected %d num ref, observed %d num ref in [state = %s]", numRefs, 403 dec.checkedData.NumRef(), decState) 404 } 405 // all good 406 return nil 407 } 408 // validate primary 409 if err := validate(sys.primary.(*decoder), decState.primary, decState.numRefs); err != nil { 410 return &gopter.PropResult{Status: gopter.PropError, Error: err} 411 } 412 // validate all duplicates 413 for i := range sys.duplicates { 414 dec := sys.duplicates[i].(*decoder) 415 state := decState.duplicates[i] 416 if err := validate(dec, state, decState.numRefs); err != nil { 417 return &gopter.PropResult{Status: gopter.PropError, Error: err} 418 } 419 } 420 return &gopter.PropResult{Status: gopter.PropTrue} 421 } 422 423 func newDecoderState() gopter.Gen { 424 return anyASCIITags().Map( 425 func(tags ident.Tags) *multiDecoderState { 426 enc := newTestTagEncoder() 427 if err := enc.Encode(ident.NewTagsIterator(tags)); err != nil { 428 return nil 429 } 430 b, ok := enc.Data() 431 if !ok { 432 return nil 433 } 434 data := checked.NewBytes(b.Bytes(), nil) 435 return &multiDecoderState{ 436 tags: tags, 437 initBytes: data, 438 numRefs: 1, 439 primary: decoderState{ 440 numTags: len(tags.Values()), 441 }, 442 } 443 }, 444 ) 445 } 446 447 func anyASCIITag() gopter.Gen { 448 return gopter.CombineGens(gen.Identifier(), gen.Identifier()). 449 Map(func(values []interface{}) ident.Tag { 450 name := values[0].(string) 451 value := values[1].(string) 452 return ident.StringTag(name, value) 453 }) 454 } 455 456 func anyASCIITags() gopter.Gen { 457 return gen.SliceOf(anyASCIITag()). 458 Map(func(tags []ident.Tag) ident.Tags { 459 return ident.NewTags(tags...) 460 }) 461 } 462 463 func (d decoderState) String() string { 464 return fmt.Sprintf("[ numTags=%d, closed=%v, numNextCalls=%d ]", 465 d.numTags, d.closed, d.numNextCalls) 466 } 467 468 func (d *decoderState) hasCurrentTagsReference() bool { 469 return !d.closed && 470 d.numTags > 0 && 471 d.numNextCalls > 0 && 472 d.numNextCalls <= d.numTags 473 } 474 475 func (d decoderState) numRemaining() int { 476 if d.closed { 477 return 0 478 } 479 remain := d.numTags - d.numNextCalls 480 if remain >= 0 { 481 return remain 482 } 483 return 0 484 } 485 486 func (d multiDecoderState) String() string { 487 var buf bytes.Buffer 488 489 buf.WriteString(fmt.Sprintf("[ numRefs=%d, tags=[%s], primary=%s ", 490 d.numRefs, tagsToString(d.tags), d.primary.String())) 491 492 for i, dupe := range d.duplicates { 493 buf.WriteString(fmt.Sprintf(", dupe_%d=%s ", (i + 1), dupe.String())) 494 } 495 496 buf.WriteString("]") 497 return buf.String() 498 } 499 500 func tagsToString(tags ident.Tags) string { 501 var tagBuffer bytes.Buffer 502 for i, t := range tags.Values() { 503 if i != 0 { 504 tagBuffer.WriteString(", ") 505 } 506 tagBuffer.WriteString(t.Name.String()) 507 tagBuffer.WriteString("=") 508 tagBuffer.WriteString(t.Value.String()) 509 } 510 return tagBuffer.String() 511 }