github.com/ipld/go-ipld-prime@v0.21.0/node/tests/testcase.go (about)

     1  package tests
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"strings"
     8  	"testing"
     9  
    10  	qt "github.com/frankban/quicktest"
    11  	"github.com/polydawn/refmt/json"
    12  	"github.com/polydawn/refmt/shared"
    13  
    14  	"github.com/ipld/go-ipld-prime/codec"
    15  	"github.com/ipld/go-ipld-prime/codec/dagjson"
    16  	"github.com/ipld/go-ipld-prime/datamodel"
    17  	"github.com/ipld/go-ipld-prime/schema"
    18  	"github.com/ipld/go-ipld-prime/traversal"
    19  )
    20  
    21  // This file introduces a testcase struct and a bunch of functions around it.
    22  //  This structure can be used to specify many test scenarios easily, using json as a shorthand for the fixtures.
    23  //  Not everything can be tested this way (in particular, there's some fun details around maps with complex keys, and structs with absent fields), but it covers a lot.
    24  
    25  /*
    26  testcase contains data for directing a sizable number of tests against a NodePrototype
    27  (or more specifically, a pair of them -- one for the type-level node, one for the representation),
    28  all of which are applied by calling the testcase.Test method:
    29  
    30    - Creation of values using the type-level builder is tested.
    31    - This is done using a json input as a convenient shorthand.
    32    - n.b. this is optional, because it won't work for maps with complex keys.
    33    - In things that behave as maps: this tests the AssembleEntry path (rather than AssembleKey+AssembleValue; this is the case because this is implemented using unmarshal codepaths).
    34    - If this is expected to fail, an expected error may be specified (which will also make all other tests after creation inapplicable to this testcase).
    35    - Creation of values using the repr-level builder is tested.
    36    - This is (again) done using a json input as a convenient shorthand.
    37    - At least *one* of this or the json for type-level must be present.  If neither: the testcase spec is broken.
    38    - As for the type-level test: in things that behave as maps, this tests the AssembleEntry path.
    39    - If this is expected to fail, an expected error may be specified (which will also make all other tests after creation inapplicable to this testcase).
    40    - If both forms of creation were exercised: check that the result nodes are deep-equal.
    41    - A list of "point" observations may be provided, which can probe positions in the data tree for expected values (or just type kind, etc).
    42    - This tests that direct lookups work.  (It doesn't test iterators; that'll come in another step, later.)
    43    - Pathing (a la traversal.Get) is used for this this, so it's ready to inspect deep structures.
    44    - The field for expected value is just `interface{}`; it handles nodes, some primitives, and will also allow asserting an error.
    45    - The node is *copied*, and deep-equal checked again.
    46    - The purpose of this is to exercise the AssembleKey+AssembleValue path (as opposed to AssembleEntry (which is already exercised by our creation tests, since they use unmarshal codepaths)).
    47    - Access of type-level data via iterators is tested in one of two ways:
    48    - A list of expected key+values expected of the iterator can be provided explicitly;
    49    - If an explicit list isn't provided, but type-level json is provided, the type-level data will be marshalled and compared to the json fixture.
    50    - Most things can use the json path -- those that can't (e.g. maps with complex keys; structs with absent values -- neither is marshallable) use the explicit key+value system instead.
    51    - Access of the representation-level data via interators is tested via marshalling, and asserting it against the json fixture data (if present).
    52    - There's no explicit key+value list alternative here -- it's not needed; there is no data that is unmarshallable, by design!
    53  
    54  This system should cover a lot of things, but doesn't cover everything.
    55  
    56    - Good coverage for "reset" pathways is reached somewhat indirectly...
    57    - Tests for recursive types containing nontrivial reset methods exercise both the child type's assembler reset method, and that the parent calls it correctly.
    58    - Maps with complex keys are tricky to handle, as already noted above.
    59    - But you should be able to do it, with some care.
    60    - This whole system depends on json parsers and serializers already working.
    61    - This is arguably an uncomfortably large and complex dependency for a test system.  However, the json systems are tested by using basicnode; there's no cycle here.
    62    - "Unhappy paths" in creation are a bit tricky to test.
    63    - It can be done, but for map-like things, only for the AssembleEntry path.
    64    - PRs welcome if someone's got a clever idea for a good way to exercise AssembleKey+AssembleValue.  (A variant of unmarshaller implementation?  Would do it; just verbose.)
    65    - No support yet for checking properties like Length.
    66    - Future: we could add another type-hinted special case to the testcasePoint.expect for this, i suppose.
    67  */
    68  type testcase struct {
    69  	name                string          // name for the testcase.
    70  	typeJson            string          // json that will be fed to unmarshal together with a type-level assembler.  marshal output will also be checked for equality.  may be absent.
    71  	reprJson            string          // json that will be fed to unmarshal together with a representational assembler.  marshal output will also be checked for equality.
    72  	expectUnmarshalFail error           // if present, this error will be expected from the unmarshal process (and implicitly, marshal tests will not be applicable for this testcase).
    73  	typePoints          []testcasePoint // inspections that will be made by traversing the type-level nodes.
    74  	reprPoints          []testcasePoint // inspections that will be made by traversing the representation nodes.
    75  	typeItr             []entry         // if set, the type will be iterated in this way.  The remarshalling and checking against typeJson will not be tested.  This is used to probe for correct iteration over Absent values in structs (which needs special handling, because they are unserializable).
    76  	// there's really no need for an 'expectFail' that applies to marshal, because it shouldn't be possible to create data that's unmarshallable!  (excepting data which is not marshallable by some *codec* due to incompleteness of that codec.  But that's not what we're testing, here.)
    77  	// there's no need for a reprItr because the marshalling to reprJson always covers that; unlike with the type level, neither absents nor complex keys can throw a wrench in serialization, so it's always available to us to exercise the iteration code.
    78  }
    79  
    80  type testcasePoint struct {
    81  	path   string
    82  	expect interface{} // if primitive: we'll AsFoo and assert equal on that; if an error, we'll expect an error and compare error types; if a kind, we'll check that the thing reached simply has that kind.
    83  }
    84  
    85  type entry struct {
    86  	key   interface{} // (mostly string.  not yet defined how this will handle maps with complex keys.)
    87  	value interface{} // same rules as testcasePoint.expect
    88  }
    89  
    90  func (tcase testcase) Test(t *testing.T, np, npr datamodel.NodePrototype) {
    91  	t.Run(tcase.name, func(t *testing.T) {
    92  		// We'll produce either one or two nodes, depending on the fixture; if two, we'll be expecting them to be equal.
    93  		var n, n2 datamodel.Node
    94  
    95  		// Attempt to produce a node by using unmarshal on type-level fixture data and the type-level NodePrototype.
    96  		//  This exercises creating a value using the AssembleEntry path (but note, not AssembleKey+AssembleValue path).
    97  		//  This test section is optional because we can't use it for some types (namely, maps with complex keys -- which simply need custom tests).
    98  		if tcase.typeJson != "" {
    99  			t.Run("typed-create", func(t *testing.T) {
   100  				n = testUnmarshal(t, np, tcase.typeJson, tcase.expectUnmarshalFail)
   101  			})
   102  		}
   103  
   104  		// Attempt to produce a node by using unmarshal on repr-level fixture data and the repr-level NodePrototype.
   105  		//  This exercises creating a value using the AssembleEntry path (but note, not AssembleKey+AssembleValue path).
   106  		//  This test section is optional simply because it's nice to be able to omit it when writing a new system and not wanting to test representation yet.
   107  		if tcase.reprJson != "" {
   108  			t.Run("repr-create", func(t *testing.T) {
   109  				n3 := testUnmarshal(t, npr, tcase.reprJson, tcase.expectUnmarshalFail)
   110  				if n == nil {
   111  					n = n3
   112  				} else {
   113  					n2 = n3
   114  				}
   115  			})
   116  		}
   117  
   118  		// If unmarshalling was expected to fail, the rest of the tests are inapplicable.
   119  		if tcase.expectUnmarshalFail != nil {
   120  			return
   121  		}
   122  
   123  		// Check the nodes are equal, if there's two of them.  (Or holler, if none.)
   124  		if n == nil {
   125  			t.Fatalf("invalid fixture: need one of either typeJson or reprJson provided")
   126  		}
   127  		if n2 != nil {
   128  			t.Run("type-create and repr-create match", func(t *testing.T) {
   129  				qt.Check(t, n, NodeContentEquals, n2)
   130  			})
   131  		}
   132  
   133  		// Perform all the point inspections on the type-level node.
   134  		if tcase.typePoints != nil {
   135  			t.Run("type-level inspection", func(t *testing.T) {
   136  				for _, point := range tcase.typePoints {
   137  					wishPoint(t, n, point)
   138  				}
   139  			})
   140  		}
   141  
   142  		// Perform all the point inspections on the repr-level node.
   143  		if tcase.reprPoints != nil {
   144  			t.Run("repr-level inspection", func(t *testing.T) {
   145  				for _, point := range tcase.reprPoints {
   146  					wishPoint(t, n.(schema.TypedNode).Representation(), point)
   147  				}
   148  			})
   149  		}
   150  
   151  		// Serialize the type-level node, and check that we get the original json again.
   152  		//  This exercises iterators on the type-level node.
   153  		//  OR, if typeItr is present, do that instead (this is necessary when handling maps with complex keys or handling structs with absent values, since both of those are unserializable).
   154  		if tcase.typeItr != nil {
   155  			// This can unconditionally assume we're going to handle maps,
   156  			//  because the only kind of thing that needs this style of testing are some instances of maps and some instances of structs.
   157  			itr := n.MapIterator()
   158  			for _, entry := range tcase.typeItr {
   159  				qt.Check(t, itr.Done(), qt.IsFalse)
   160  				k, v, err := itr.Next()
   161  				qt.Check(t, k, closeEnough, entry.key)
   162  				qt.Check(t, v, closeEnough, entry.value)
   163  				qt.Check(t, err, qt.IsNil)
   164  			}
   165  			qt.Check(t, itr.Done(), qt.IsTrue)
   166  			k, v, err := itr.Next()
   167  			qt.Check(t, k, qt.IsNil)
   168  			qt.Check(t, v, qt.IsNil)
   169  			qt.Check(t, err, qt.Equals, datamodel.ErrIteratorOverread{})
   170  		} else if tcase.typeJson != "" {
   171  			t.Run("type-marshal", func(t *testing.T) {
   172  				testMarshal(t, n, tcase.typeJson)
   173  			})
   174  		}
   175  
   176  		// Serialize the repr-level node, and check that we get the original json again.
   177  		//  This exercises iterators on the repr-level node.
   178  		if tcase.reprJson != "" {
   179  			t.Run("repr-marshal", func(t *testing.T) {
   180  				testMarshal(t, n.(schema.TypedNode).Representation(), tcase.reprJson)
   181  			})
   182  		}
   183  
   184  		// Copy the node.  If it's a map-like.
   185  		//  This exercises the AssembleKey+AssembleValue path for maps (or things that act as maps, such as structs and unions),
   186  		//   as opposed to the AssembleEntry path (which is what was exercised by the creation via unmarshal).
   187  		// Assumes that the iterators are working correctly.
   188  		if n.Kind() == datamodel.Kind_Map {
   189  			t.Run("type-create with AK+AV", func(t *testing.T) {
   190  				n3, err := shallowCopyMap(np, n)
   191  				qt.Check(t, err, qt.IsNil)
   192  				qt.Check(t, n, NodeContentEquals, n3)
   193  			})
   194  		}
   195  
   196  		// Copy the node, now at repr level.  Again, this is for exercising AssembleKey+AssembleValue paths.
   197  		// Assumes that the iterators are working correctly.
   198  		if n.(schema.TypedNode).Representation().Kind() == datamodel.Kind_Map {
   199  			t.Run("repr-create with AK+AV", func(t *testing.T) {
   200  				n3, err := shallowCopyMap(npr, n.(schema.TypedNode).Representation())
   201  				qt.Check(t, err, qt.IsNil)
   202  				qt.Check(t, n3, NodeContentEquals, n)
   203  			})
   204  		}
   205  
   206  	})
   207  }
   208  
   209  func shallowCopyMap(np datamodel.NodePrototype, n datamodel.Node) (datamodel.Node, error) {
   210  	nb := np.NewBuilder()
   211  	ma, err := nb.BeginMap(n.Length())
   212  	if err != nil {
   213  		return nil, err
   214  	}
   215  	for itr := n.MapIterator(); !itr.Done(); {
   216  		k, v, err := itr.Next()
   217  		if err != nil {
   218  			return nil, err
   219  		}
   220  		if v.IsAbsent() {
   221  			continue
   222  		}
   223  		if err := ma.AssembleKey().AssignNode(k); err != nil {
   224  			return nil, err
   225  		}
   226  		if err := ma.AssembleValue().AssignNode(v); err != nil {
   227  			return nil, err
   228  		}
   229  	}
   230  	if err := ma.Finish(); err != nil {
   231  		return nil, err
   232  	}
   233  	return nb.Build(), nil
   234  }
   235  
   236  func testUnmarshal(t *testing.T, np datamodel.NodePrototype, data string, expectFail error) datamodel.Node {
   237  	t.Helper()
   238  	nb := np.NewBuilder()
   239  	err := dagjson.Decode(nb, strings.NewReader(data))
   240  	switch {
   241  	case expectFail == nil && err != nil:
   242  		t.Fatalf("fixture parse failed: %s", err)
   243  	case expectFail == nil && err == nil:
   244  		// carry on
   245  	case expectFail != nil && err != nil:
   246  		qt.Check(t, err, qt.ErrorAs, expectFail)
   247  	case expectFail != nil && err == nil:
   248  		t.Errorf("expected creation to fail with a %T error, but got no error", expectFail)
   249  	}
   250  	return nb.Build()
   251  }
   252  
   253  func testMarshal(t *testing.T, n datamodel.Node, data string) {
   254  	t.Helper()
   255  	// We'll marshal with "pretty" linebreaks and indents (and re-format the fixture to the same) for better diffing.
   256  	prettyprint := json.EncodeOptions{Line: []byte{'\n'}, Indent: []byte{'\t'}}
   257  	var buf bytes.Buffer
   258  	err := dagjson.Marshal(n, json.NewEncoder(&buf, prettyprint), dagjson.EncodeOptions{
   259  		EncodeLinks: true,
   260  		EncodeBytes: true,
   261  		MapSortMode: codec.MapSortMode_Lexical,
   262  	})
   263  	if err != nil {
   264  		t.Errorf("marshal failed: %s", err)
   265  	}
   266  	qt.Check(t, buf.String(), qt.Equals, reformat(data, prettyprint))
   267  }
   268  
   269  func wishPoint(t *testing.T, n datamodel.Node, point testcasePoint) {
   270  	t.Helper()
   271  	reached, err := traversal.Get(n, datamodel.ParsePath(point.path))
   272  	switch point.expect.(type) {
   273  	case error:
   274  		qt.Check(t, err, qt.ErrorAs, point.expect)
   275  		qt.Check(t, err, qt.Equals, point.expect)
   276  	default:
   277  		qt.Check(t, err, qt.IsNil)
   278  		if reached == nil {
   279  			return
   280  		}
   281  		qt.Check(t, reached, closeEnough, point.expect)
   282  	}
   283  }
   284  
   285  // closeEnough conforms to quicktest.Checker (so we can use it in quicktest invocations),
   286  // and lets Nodes be compared to primitives in convenient ways.
   287  //
   288  // If the expected value is a primitive string, it'll AsStrong on the Node; etc.
   289  //
   290  // Using a datamodel.Kind value is also possible, which will just check the kind and not the value contents.
   291  //
   292  // If a datamodel.Node is the expected value, a full deep qt.Equals is used as normal.
   293  var closeEnough = &closeEnoughChecker{}
   294  
   295  var _ qt.Checker = (*closeEnoughChecker)(nil)
   296  
   297  type closeEnoughChecker struct{}
   298  
   299  func (c *closeEnoughChecker) ArgNames() []string {
   300  	return []string{"got", "want"}
   301  }
   302  
   303  func (c *closeEnoughChecker) Check(actual interface{}, args []interface{}, note func(key string, value interface{})) (err error) {
   304  	expected := args[0]
   305  	if expected == nil {
   306  		return qt.IsNil.Check(actual, args, note)
   307  	}
   308  	a, ok := actual.(datamodel.Node)
   309  	if !ok {
   310  		return errors.New("this checker only supports checking datamodel.Node values")
   311  	}
   312  	switch expected.(type) {
   313  	case datamodel.Kind:
   314  		return qt.Equals.Check(a.Kind(), args, note)
   315  	case string:
   316  		if a.Kind() != datamodel.Kind_String {
   317  			return fmt.Errorf("expected something with kind string, got kind %s", a.Kind())
   318  		}
   319  		x, _ := a.AsString()
   320  		return qt.Equals.Check(x, args, note)
   321  	case int:
   322  		if a.Kind() != datamodel.Kind_Int {
   323  			return fmt.Errorf("expected something with kind int, got kind %s", a.Kind())
   324  		}
   325  		x, _ := a.AsInt()
   326  		return qt.Equals.Check(x, args, note)
   327  	case datamodel.Node:
   328  		return qt.Equals.Check(actual, args, note)
   329  	default:
   330  		return fmt.Errorf("this checker doesn't support an expected value of type %T", expected)
   331  	}
   332  }
   333  
   334  func reformat(x string, opts json.EncodeOptions) string {
   335  	var buf bytes.Buffer
   336  	if err := (shared.TokenPump{
   337  		TokenSource: json.NewDecoder(strings.NewReader(x)),
   338  		TokenSink:   json.NewEncoder(&buf, opts),
   339  	}).Run(); err != nil {
   340  		panic(err)
   341  	}
   342  	return buf.String()
   343  }