github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/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  }