github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/query/graphite/native/compiler_test.go (about)

     1  // Copyright (c) 2019 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 native
    22  
    23  import (
    24  	"fmt"
    25  	"math"
    26  	"testing"
    27  
    28  	"github.com/m3db/m3/src/query/graphite/common"
    29  	"github.com/m3db/m3/src/query/graphite/lexer"
    30  	"github.com/m3db/m3/src/query/graphite/storage"
    31  	graphitetest "github.com/m3db/m3/src/query/graphite/testing"
    32  	"github.com/m3db/m3/src/query/graphite/ts"
    33  	xtest "github.com/m3db/m3/src/x/test"
    34  
    35  	"github.com/golang/mock/gomock"
    36  	"github.com/stretchr/testify/assert"
    37  	"github.com/stretchr/testify/require"
    38  )
    39  
    40  type testCompile struct {
    41  	input  string
    42  	result interface{}
    43  }
    44  
    45  func hello(ctx *common.Context) (string, error)         { return "hello", nil }
    46  func noArgs(ctx *common.Context) (ts.SeriesList, error) { return ts.SeriesList{}, nil }
    47  func defaultArgs(ctx *common.Context, b bool, f1, f2 float64, s string) (ts.SeriesList, error) {
    48  	return ts.SeriesList{}, nil
    49  }
    50  
    51  func TestCompile1(t *testing.T) {
    52  	var (
    53  		sortByName        = findFunction("sortByName")
    54  		noArgs            = findFunction("noArgs")
    55  		aliasByNode       = findFunction("aliasByNode")
    56  		summarize         = findFunction("summarize")
    57  		defaultArgs       = findFunction("defaultArgs")
    58  		sumSeries         = findFunction("sumSeries")
    59  		asPercent         = findFunction("asPercent")
    60  		scale             = findFunction("scale")
    61  		logarithm         = findFunction("logarithm")
    62  		removeEmptySeries = findFunction("removeEmptySeries")
    63  		filterSeries      = findFunction("filterSeries")
    64  	)
    65  
    66  	tests := []testCompile{
    67  		{"", noopExpression{}},
    68  		{"foobar", newFetchExpression("foobar")},
    69  		{
    70  			"foo.bar.{a,b,c}.baz-*.stat[0-9]",
    71  			newFetchExpression("foo.bar.{a,b,c}.baz-*.stat[0-9]"),
    72  		},
    73  		{"noArgs()", &funcExpression{&functionCall{f: noArgs}}},
    74  		{"sortByName(foo.bar.zed)", &funcExpression{
    75  			&functionCall{
    76  				f: sortByName,
    77  				in: []funcArg{
    78  					newFetchExpression("foo.bar.zed"),
    79  				},
    80  			},
    81  		}},
    82  		{"aliasByNode(foo.bar4.*.metrics.written, 2, 4)", &funcExpression{
    83  			&functionCall{
    84  				f: aliasByNode,
    85  				in: []funcArg{
    86  					newFetchExpression("foo.bar4.*.metrics.written"),
    87  					newIntConst(2),
    88  					newIntConst(4),
    89  				},
    90  			},
    91  		}},
    92  		{"summarize(foo.bar.baz.quux, \"1h\", \"max\", TRUE)", &funcExpression{
    93  			&functionCall{
    94  				f: summarize,
    95  				in: []funcArg{
    96  					newFetchExpression("foo.bar.baz.quux"),
    97  					newStringConst("1h"),
    98  					newStringConst("max"),
    99  					newBoolConst(true),
   100  				},
   101  			},
   102  		}},
   103  		{"summarize(foo.bar.baz.quuz, \"1h\")", &funcExpression{
   104  			&functionCall{
   105  				f: summarize,
   106  				in: []funcArg{
   107  					newFetchExpression("foo.bar.baz.quuz"),
   108  					newStringConst("1h"),
   109  					newStringConst(""),
   110  					newBoolConst(false),
   111  				},
   112  			},
   113  		}},
   114  		{"defaultArgs(true)", &funcExpression{
   115  			&functionCall{
   116  				f: defaultArgs,
   117  				in: []funcArg{
   118  					newBoolConst(true),          // non-default value
   119  					newFloat64Const(math.NaN()), // default value
   120  					newFloat64Const(100),        // default value
   121  					newStringConst("foobar"),    // default value
   122  				},
   123  			},
   124  		}},
   125  		{"sortByName(aliasByNode(foo.bar72.*.metrics.written,2,4,6))", &funcExpression{
   126  			&functionCall{
   127  				f: sortByName,
   128  				in: []funcArg{
   129  					&functionCall{
   130  						f: aliasByNode,
   131  						in: []funcArg{
   132  							newFetchExpression("foo.bar72.*.metrics.written"),
   133  							newIntConst(2),
   134  							newIntConst(4),
   135  							newIntConst(6),
   136  						},
   137  					},
   138  				},
   139  			},
   140  		}},
   141  		{"sumSeries(foo.bar.baz.quux, foo.bar72.*.metrics.written)", &funcExpression{
   142  			&functionCall{
   143  				f: sumSeries,
   144  				in: []funcArg{
   145  					newFetchExpression("foo.bar.baz.quux"),
   146  					newFetchExpression("foo.bar72.*.metrics.written"),
   147  				},
   148  			},
   149  		}},
   150  		{"asPercent(foo.bar72.*.metrics.written, foo.bar.baz.quux)", &funcExpression{
   151  			&functionCall{
   152  				f: asPercent,
   153  				in: []funcArg{
   154  					newFetchExpression("foo.bar72.*.metrics.written"),
   155  					newFetchExpression("foo.bar.baz.quux"),
   156  				},
   157  			},
   158  		}},
   159  		{"asPercent(foo.bar72.*.metrics.written, sumSeries(foo.bar.baz.quux))", &funcExpression{
   160  			&functionCall{
   161  				f: asPercent,
   162  				in: []funcArg{
   163  					newFetchExpression("foo.bar72.*.metrics.written"),
   164  					&functionCall{
   165  						f: sumSeries,
   166  						in: []funcArg{
   167  							newFetchExpression("foo.bar.baz.quux"),
   168  						},
   169  					},
   170  				},
   171  			},
   172  		}},
   173  		{"asPercent(foo.bar72.*.metrics.written, 100)", &funcExpression{
   174  			&functionCall{
   175  				f: asPercent,
   176  				in: []funcArg{
   177  					newFetchExpression("foo.bar72.*.metrics.written"),
   178  					newIntConst(100),
   179  				},
   180  			},
   181  		}},
   182  		{"asPercent(foo.bar72.*.metrics.written)", &funcExpression{
   183  			&functionCall{
   184  				f: asPercent,
   185  				in: []funcArg{
   186  					newFetchExpression("foo.bar72.*.metrics.written"),
   187  					newConstArg([]*ts.Series(nil)),
   188  				},
   189  			},
   190  		}},
   191  		{"asPercent(foo.bar72.*.metrics.written, total=sumSeries(foo.bar.baz.quux))", &funcExpression{
   192  			&functionCall{
   193  				f: asPercent,
   194  				in: []funcArg{
   195  					newFetchExpression("foo.bar72.*.metrics.written"),
   196  					&functionCall{
   197  						f: sumSeries,
   198  						in: []funcArg{
   199  							newFetchExpression("foo.bar.baz.quux"),
   200  						},
   201  					},
   202  				},
   203  			},
   204  		}},
   205  		{"asPercent(foo.bar72.*.metrics.written, total=100)", &funcExpression{
   206  			&functionCall{
   207  				f: asPercent,
   208  				in: []funcArg{
   209  					newFetchExpression("foo.bar72.*.metrics.written"),
   210  					newIntConst(100),
   211  				},
   212  			},
   213  		}},
   214  		{"scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1e+3)", &funcExpression{
   215  			&functionCall{
   216  				f: scale,
   217  				in: []funcArg{
   218  					newFetchExpression("servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*"),
   219  					newFloat64Const(1000),
   220  				},
   221  			},
   222  		}},
   223  		{"scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1e-3)", &funcExpression{
   224  			&functionCall{
   225  				f: scale,
   226  				in: []funcArg{
   227  					newFetchExpression("servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*"),
   228  					newFloat64Const(0.001),
   229  				},
   230  			},
   231  		}},
   232  		{"scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1e3)", &funcExpression{
   233  			&functionCall{
   234  				f: scale,
   235  				in: []funcArg{
   236  					newFetchExpression("servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*"),
   237  					newFloat64Const(1000),
   238  				},
   239  			},
   240  		}},
   241  		{"scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1.1e3)", &funcExpression{
   242  			&functionCall{
   243  				f: scale,
   244  				in: []funcArg{
   245  					newFetchExpression("servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*"),
   246  					newFloat64Const(1100),
   247  				},
   248  			},
   249  		}},
   250  		{"scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1.1e+3)", &funcExpression{
   251  			&functionCall{
   252  				f: scale,
   253  				in: []funcArg{
   254  					newFetchExpression("servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*"),
   255  					newFloat64Const(1100),
   256  				},
   257  			},
   258  		}},
   259  		{"scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1.2e-3)", &funcExpression{
   260  			&functionCall{
   261  				f: scale,
   262  				in: []funcArg{
   263  					newFetchExpression("servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*"),
   264  					newFloat64Const(0.0012),
   265  				},
   266  			},
   267  		}},
   268  		{"scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, .1e+3)", &funcExpression{
   269  			&functionCall{
   270  				f: scale,
   271  				in: []funcArg{
   272  					newFetchExpression("servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*"),
   273  					newFloat64Const(100),
   274  				},
   275  			},
   276  		}},
   277  		{"scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 2.e+3)", &funcExpression{
   278  			&functionCall{
   279  				f: scale,
   280  				in: []funcArg{
   281  					newFetchExpression("servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*"),
   282  					newFloat64Const(2000),
   283  				},
   284  			},
   285  		}},
   286  		{"logarithm(a.b.c)", &funcExpression{
   287  			&functionCall{
   288  				f: logarithm,
   289  				in: []funcArg{
   290  					newFetchExpression("a.b.c"),
   291  					newFloat64Const(10),
   292  				},
   293  			},
   294  		}},
   295  		{"removeEmptySeries(a.b.c)", &funcExpression{
   296  			&functionCall{
   297  				f: removeEmptySeries,
   298  				in: []funcArg{
   299  					newFetchExpression("a.b.c"),
   300  					newFloat64Const(0),
   301  				},
   302  			},
   303  		}},
   304  		{"filterSeries(a.b.c, 'max', '>', 1000)", &funcExpression{
   305  			&functionCall{
   306  				f: filterSeries,
   307  				in: []funcArg{
   308  					newFetchExpression("a.b.c"),
   309  					newStringConst("max"),
   310  					newStringConst(">"),
   311  					newFloat64Const(1000),
   312  				},
   313  			},
   314  		}},
   315  	}
   316  
   317  	ctrl := xtest.NewController(t)
   318  	defer ctrl.Finish()
   319  
   320  	ctx := common.NewTestContext()
   321  	store := storage.NewMockStorage(ctrl)
   322  	store.EXPECT().FetchByQuery(gomock.Any(), gomock.Any(), gomock.Any()).
   323  		Return(&storage.FetchResult{}, nil).AnyTimes()
   324  	ctx.Engine = NewEngine(store, CompileOptions{})
   325  
   326  	for _, test := range tests {
   327  		expr, err := Compile(test.input, CompileOptions{})
   328  		require.NoError(t, err, "error compiling: expression='%s', error='%v'", test.input, err)
   329  		require.NotNil(t, expr)
   330  		assertExprTree(t, test.result, expr, fmt.Sprintf("invalid result for %s: %v vs %v",
   331  			test.input, test.result, expr))
   332  
   333  		// Ensure that the function can execute.
   334  		_, err = expr.Execute(ctx)
   335  		require.NoError(t, err)
   336  	}
   337  }
   338  
   339  type testCompilerError struct {
   340  	input string
   341  	err   string
   342  }
   343  
   344  func TestCompileErrors(t *testing.T) {
   345  	tests := []testCompilerError{
   346  		{"hello()", "top-level functions must return timeseries data"},
   347  		{"foobar(", "invalid expression 'foobar(': could not find function named foobar"},
   348  		{"foobar()", "invalid expression 'foobar()': could not find function named foobar"},
   349  		{"sortByName(foo.*.zed)junk", "invalid expression 'sortByName(foo.*.zed)junk': " +
   350  			"extra data junk"},
   351  		{
   352  			"aliasByNode(",
   353  			"invalid expression 'aliasByNode(': unexpected eof while parsing aliasByNode",
   354  		},
   355  		{
   356  			"unknownFunc()",
   357  			"invalid expression 'unknownFunc()': could not find function named unknownFunc",
   358  		},
   359  		{
   360  			"aliasByNode(10)",
   361  			"invalid expression 'aliasByNode(10)': invalid function call aliasByNode," +
   362  				" arg 0: expected a singlePathSpec, received a float64 '10'",
   363  		},
   364  		{
   365  			"sortByName(hello())",
   366  			"invalid expression 'sortByName(hello())': invalid function call " +
   367  				"sortByName, arg 0: expected a singlePathSpec, received a functionCall 'hello()'",
   368  		},
   369  		{
   370  			"aliasByNode()",
   371  			"invalid expression 'aliasByNode()': invalid number of arguments for aliasByNode;" +
   372  				" expected at least 2, received 0",
   373  		},
   374  		{
   375  			"aliasByNode(foo.*.zed, 2, false)",
   376  			"invalid expression 'aliasByNode(foo.*.zed, 2, false)': invalid function call " +
   377  				"aliasByNode, arg 2: expected a int, received a bool 'false'",
   378  		},
   379  		{
   380  			"aliasByNode(foo.*.bar,",
   381  			"invalid expression 'aliasByNode(foo.*.bar,': unexpected eof while" +
   382  				" parsing aliasByNode",
   383  		},
   384  		{
   385  			"aliasByNode(foo.*.bar,)",
   386  			"invalid expression 'aliasByNode(foo.*.bar,)': invalid function call" +
   387  				" aliasByNode, arg 1: invalid expression 'aliasByNode(foo.*.bar,)': ) not valid",
   388  		},
   389  		// TODO(jayp): Not providing all required parameters in a function with default parameters
   390  		// leads to an error message that states that a greater than required number of expected
   391  		// arguments. We could do better, but punting for now.
   392  		{
   393  			"summarize(foo.bar.baz.quux)",
   394  			"invalid expression 'summarize(foo.bar.baz.quux)':" +
   395  				" invalid number of arguments for summarize; expected 4, received 1",
   396  		},
   397  		{
   398  			"sumSeries(foo.bar.baz.quux,)",
   399  			"invalid expression 'sumSeries(foo.bar.baz.quux,)': invalid function call sumSeries, " +
   400  				"arg 1: invalid expression 'sumSeries(foo.bar.baz.quux,)': ) not valid",
   401  		},
   402  		{
   403  			"asPercent(foo.bar72.*.metrics.written, total",
   404  			"invalid expression 'asPercent(foo.bar72.*.metrics.written, total': " +
   405  				"invalid function call asPercent, " +
   406  				"arg 1: invalid expression 'asPercent(foo.bar72.*.metrics.written, total': " +
   407  				"unexpected eof, total should be followed by = or (",
   408  		},
   409  		{
   410  			"asPercent(foo.bar72.*.metrics.written, total=",
   411  			"invalid expression 'asPercent(foo.bar72.*.metrics.written, total=': " +
   412  				"invalid function call asPercent, " +
   413  				"arg 1: invalid expression 'asPercent(foo.bar72.*.metrics.written, total=': " +
   414  				"unexpected eof, named argument total should be followed by its value",
   415  		},
   416  		{
   417  			"asPercent(foo.bar72.*.metrics.written, total=randomStuff",
   418  			"invalid expression 'asPercent(foo.bar72.*.metrics.written, total=randomStuff': " +
   419  				"invalid function call asPercent, " +
   420  				"arg 1: invalid expression 'asPercent(foo.bar72.*.metrics.written, total=randomStuff': " +
   421  				"unexpected eof, randomStuff should be followed by = or (",
   422  		},
   423  		{
   424  			"asPercent(foo.bar72.*.metrics.written, total=sumSeries(",
   425  			"invalid expression 'asPercent(foo.bar72.*.metrics.written, total=sumSeries(': " +
   426  				"invalid function call asPercent, " +
   427  				"arg 1: invalid expression 'asPercent(foo.bar72.*.metrics.written, total=sumSeries(': " +
   428  				"unexpected eof while parsing sumSeries",
   429  		},
   430  		{
   431  			"scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1.e)",
   432  			"invalid expression 'scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1.e)': " +
   433  				"invalid function call scale, " +
   434  				"arg 1: invalid expression 'scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1.e)': " +
   435  				"expected one of 0123456789, found ) not valid",
   436  		},
   437  		{
   438  			"scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, .1e)",
   439  			"invalid expression 'scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, .1e)': " +
   440  				"invalid function call scale, " +
   441  				"arg 1: invalid expression 'scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, .1e)': " +
   442  				"expected one of 0123456789, found ) not valid",
   443  		},
   444  		{
   445  			"scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, .e)",
   446  			"invalid expression 'scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, .e)': " +
   447  				"invalid function call scale, " +
   448  				"arg 1: invalid expression 'scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, .e)': " +
   449  				"expected one of 0123456789, found e not valid",
   450  		},
   451  		{
   452  			"scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, e)",
   453  			"invalid expression 'scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, e)': " +
   454  				"invalid function call scale, " +
   455  				"arg 1: expected a float64, received a fetchExpression 'fetch(e)'",
   456  		},
   457  		{
   458  			"scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1.2ee)",
   459  			"invalid expression 'scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1.2ee)': " +
   460  				"invalid function call scale, " +
   461  				"arg 1: invalid expression 'scale(servers.foobar*-qaz.quail.qux-qaz-qab.cpu.*, 1.2ee)': " +
   462  				"expected one of 0123456789, found e not valid",
   463  		},
   464  	}
   465  
   466  	for _, test := range tests {
   467  		expr, err := Compile(test.input, CompileOptions{})
   468  		require.NotNil(t, err, "no error for %s", test.input)
   469  		assert.Equal(t, test.err, err.Error(), "wrong error for %s", test.input)
   470  		assert.Nil(t, expr, "non-nil expression for %s", test.input)
   471  	}
   472  }
   473  
   474  func assertExprTree(t *testing.T, expected interface{}, actual interface{}, msg string) {
   475  	switch e := expected.(type) {
   476  	case *functionCall:
   477  		a, ok := actual.(*functionCall)
   478  		require.True(t, ok, msg)
   479  		require.Equal(t, e.f.name, a.f.name, msg)
   480  		require.Equal(t, len(e.f.in), len(a.f.in), msg)
   481  		for i := range e.in {
   482  			assertExprTree(t, e.in[i], a.in[i], msg)
   483  		}
   484  	case noopExpression:
   485  		_, ok := actual.(noopExpression)
   486  		require.True(t, ok, msg)
   487  	case *funcExpression:
   488  		a, ok := actual.(*funcExpression)
   489  		require.True(t, ok, msg)
   490  		assertExprTree(t, e.call, a.call, msg)
   491  	case *fetchExpression:
   492  		a, ok := actual.(*fetchExpression)
   493  		require.True(t, ok, msg)
   494  		assert.Equal(t, e.pathArg.path, a.pathArg.path, msg)
   495  	case constFuncArg:
   496  		a, ok := actual.(constFuncArg)
   497  		require.True(t, ok, msg)
   498  		if !a.value.IsValid() {
   499  			// Explicit nil.
   500  			require.True(t, e.value.IsZero())
   501  		} else {
   502  			graphitetest.Equalish(t, e.value.Interface(), a.value.Interface(), msg)
   503  		}
   504  	default:
   505  		assert.Equal(t, expected, actual, msg)
   506  	}
   507  }
   508  
   509  func TestExtractFetchExpressions(t *testing.T) {
   510  	tests := []struct {
   511  		expr    string
   512  		targets []string
   513  	}{
   514  		{"summarize(groupByNode(nonNegativeDerivative(foo.qaz.gauges.bar.baz.qux.foobar.*.quz.quail.count), 8, 'sum'), '10min', 'avg', true)", []string{
   515  			"foo.qaz.gauges.bar.baz.qux.foobar.*.quz.quail.count",
   516  		}},
   517  		{"asPercent(foo.bar72.*.metrics.written, total=sumSeries(foo.bar.baz.quux))", []string{
   518  			"foo.bar72.*.metrics.written", "foo.bar.baz.quux",
   519  		}},
   520  		{"foo.bar.{a,b,c}.baz-*.stat[0-9]", []string{
   521  			"foo.bar.{a,b,c}.baz-*.stat[0-9]",
   522  		}},
   523  	}
   524  
   525  	for _, test := range tests {
   526  		targets, err := ExtractFetchExpressions(test.expr)
   527  		require.NoError(t, err)
   528  		assert.Equal(t, test.targets, targets, test.expr)
   529  	}
   530  }
   531  
   532  func TestTokenLookforward(t *testing.T) {
   533  	tokenVals := []string{"a", "b", "c"}
   534  	tokens := make(chan *lexer.Token)
   535  	go func() {
   536  		for _, v := range tokenVals {
   537  			tokens <- lexer.MustMakeToken(v)
   538  		}
   539  
   540  		close(tokens)
   541  	}()
   542  
   543  	lookforward := newTokenLookforward(tokens)
   544  	token := lookforward.get()
   545  	assert.Equal(t, "a", token.Value())
   546  
   547  	// assert that peek does not iterate token.
   548  	token, found := lookforward.peek()
   549  	assert.True(t, found)
   550  	assert.Equal(t, "b", token.Value())
   551  	token, found = lookforward.peek()
   552  	assert.True(t, found)
   553  	assert.Equal(t, "b", token.Value())
   554  
   555  	// assert that next get after peek will iterate forward.
   556  	token = lookforward.get()
   557  	assert.Equal(t, "b", token.Value())
   558  	token = lookforward.get()
   559  	assert.Equal(t, "c", token.Value())
   560  
   561  	// assert peek is empty once channel is closed.
   562  	_, found = lookforward.peek()
   563  	assert.False(t, found)
   564  }
   565  
   566  func init() {
   567  	MustRegisterFunction(noArgs)
   568  	MustRegisterFunction(hello)
   569  	MustRegisterFunction(defaultArgs).WithDefaultParams(map[uint8]interface{}{
   570  		1: false,
   571  		2: math.NaN(),
   572  		3: 100.0,
   573  		4: "foobar",
   574  	})
   575  }