github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/gnovm/cmd/gno/test.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"flag"
     8  	"fmt"
     9  	"log"
    10  	"os"
    11  	"path/filepath"
    12  	"runtime/debug"
    13  	"sort"
    14  	"strings"
    15  	"text/template"
    16  	"time"
    17  
    18  	"go.uber.org/multierr"
    19  
    20  	"github.com/gnolang/gno/gnovm/pkg/gnoenv"
    21  	gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
    22  	"github.com/gnolang/gno/gnovm/pkg/gnomod"
    23  	"github.com/gnolang/gno/gnovm/pkg/transpiler"
    24  	"github.com/gnolang/gno/gnovm/tests"
    25  	"github.com/gnolang/gno/tm2/pkg/commands"
    26  	"github.com/gnolang/gno/tm2/pkg/errors"
    27  	"github.com/gnolang/gno/tm2/pkg/random"
    28  	"github.com/gnolang/gno/tm2/pkg/std"
    29  	"github.com/gnolang/gno/tm2/pkg/testutils"
    30  )
    31  
    32  type testCfg struct {
    33  	verbose             bool
    34  	rootDir             string
    35  	run                 string
    36  	timeout             time.Duration
    37  	transpile           bool // TODO: transpile should be the default, but it needs to automatically transpile dependencies in memory.
    38  	updateGoldenTests   bool
    39  	printRuntimeMetrics bool
    40  	withNativeFallback  bool
    41  }
    42  
    43  func newTestCmd(io commands.IO) *commands.Command {
    44  	cfg := &testCfg{}
    45  
    46  	return commands.NewCommand(
    47  		commands.Metadata{
    48  			Name:       "test",
    49  			ShortUsage: "test [flags] <package> [<package>...]",
    50  			ShortHelp:  "runs the tests for the specified packages",
    51  			LongHelp: `Runs the tests for the specified packages.
    52  
    53  'gno test' recompiles each package along with any files with names matching the
    54  file pattern "*_test.gno" or "*_filetest.gno".
    55  
    56  The <package> can be directory or file path (relative or absolute).
    57  
    58  - "*_test.gno" files work like "*_test.go" files, but they contain only test
    59  functions. Benchmark and fuzz functions aren't supported yet. Similarly, only
    60  tests that belong to the same package are supported for now (no "xxx_test").
    61  
    62  The package path used to execute the "*_test.gno" file is fetched from the
    63  module name found in 'gno.mod', or else it is randomly generated like
    64  "gno.land/r/XXXXXXXX".
    65  
    66  - "*_filetest.gno" files on the other hand are kind of unique. They exist to
    67  provide a way to interact and assert a gno contract, thanks to a set of
    68  specific instructions that can be added using code comments.
    69  
    70  "*_filetest.gno" must be declared in the 'main' package and so must have a
    71  'main' function, that will be executed to test the target contract.
    72  
    73  List of available instructions that can be used in "*_filetest.gno" files:
    74  	- "PKGPATH:" is a single line instruction that can be used to define the
    75  	package used to interact with the tested package. If not specified, "main" is
    76  	used.
    77  	- "MAXALLOC:" is a signle line instruction that can be used to define a limit
    78  	to the VM allocator. If this limit is exceeded, the VM will panic. Default to
    79  	0, no limit.
    80  	- "SEND:" is a single line instruction that can be used to send an amount of
    81  	token along with the transaction. The format is for example "1000000ugnot".
    82  	Default is empty.
    83  	- "Output:\n" (*) is a multiple lines instruction that can be used to assert
    84  	the output of the "*_filetest.gno" file. Any prints executed inside the
    85  	'main' function must match the lines that follows the "Output:\n"
    86  	instruction, or else the test fails.
    87  	- "Error:\n" works similarly to "Output:\n", except that it asserts the
    88  	stderr of the program, which in that case, comes from the VM because of a
    89  	panic, rather than the 'main' function.
    90  	- "Realm:\n" (*) is a multiple lines instruction that can be used to assert
    91  	what has been recorded in the store following the execution of the 'main'
    92  	function.
    93  
    94  (*) The 'update-golden-tests' flag can be set to fill out the content of the
    95  instruction with the actual content of the test instead of failing.
    96  `,
    97  		},
    98  		cfg,
    99  		func(_ context.Context, args []string) error {
   100  			return execTest(cfg, args, io)
   101  		},
   102  	)
   103  }
   104  
   105  func (c *testCfg) RegisterFlags(fs *flag.FlagSet) {
   106  	fs.BoolVar(
   107  		&c.verbose,
   108  		"v",
   109  		false,
   110  		"verbose output when running",
   111  	)
   112  
   113  	fs.BoolVar(
   114  		&c.transpile,
   115  		"transpile",
   116  		false,
   117  		"transpile gno to go before testing",
   118  	)
   119  
   120  	fs.BoolVar(
   121  		&c.updateGoldenTests,
   122  		"update-golden-tests",
   123  		false,
   124  		`writes actual as wanted for "Output:" and "Realm:" instructions`,
   125  	)
   126  
   127  	fs.StringVar(
   128  		&c.rootDir,
   129  		"root-dir",
   130  		"",
   131  		"clone location of github.com/gnolang/gno (gno tries to guess it)",
   132  	)
   133  
   134  	fs.StringVar(
   135  		&c.run,
   136  		"run",
   137  		"",
   138  		"test name filtering pattern",
   139  	)
   140  
   141  	fs.DurationVar(
   142  		&c.timeout,
   143  		"timeout",
   144  		0,
   145  		"max execution time",
   146  	)
   147  
   148  	fs.BoolVar(
   149  		&c.withNativeFallback,
   150  		"with-native-fallback",
   151  		false,
   152  		"use stdlibs/* if present, otherwise use supported native Go packages",
   153  	)
   154  
   155  	fs.BoolVar(
   156  		&c.printRuntimeMetrics,
   157  		"print-runtime-metrics",
   158  		false,
   159  		"print runtime metrics (gas, memory, cpu cycles)",
   160  	)
   161  }
   162  
   163  func execTest(cfg *testCfg, args []string, io commands.IO) error {
   164  	if len(args) < 1 {
   165  		return flag.ErrHelp
   166  	}
   167  
   168  	verbose := cfg.verbose
   169  
   170  	tempdirRoot, err := os.MkdirTemp("", "gno-transpile")
   171  	if err != nil {
   172  		log.Fatal(err)
   173  	}
   174  	defer os.RemoveAll(tempdirRoot)
   175  
   176  	// go.mod
   177  	modPath := filepath.Join(tempdirRoot, "go.mod")
   178  	err = makeTestGoMod(modPath, transpiler.ImportPrefix, "1.21")
   179  	if err != nil {
   180  		return fmt.Errorf("write .mod file: %w", err)
   181  	}
   182  
   183  	// guess opts.RootDir
   184  	if cfg.rootDir == "" {
   185  		cfg.rootDir = gnoenv.RootDir()
   186  	}
   187  
   188  	paths, err := targetsFromPatterns(args)
   189  	if err != nil {
   190  		return fmt.Errorf("list targets from patterns: %w", err)
   191  	}
   192  	if len(paths) == 0 {
   193  		io.ErrPrintln("no packages to test")
   194  		return nil
   195  	}
   196  
   197  	if cfg.timeout > 0 {
   198  		go func() {
   199  			time.Sleep(cfg.timeout)
   200  			panic("test timed out after " + cfg.timeout.String())
   201  		}()
   202  	}
   203  
   204  	subPkgs, err := gnomod.SubPkgsFromPaths(paths)
   205  	if err != nil {
   206  		return fmt.Errorf("list sub packages: %w", err)
   207  	}
   208  
   209  	buildErrCount := 0
   210  	testErrCount := 0
   211  	for _, pkg := range subPkgs {
   212  		if cfg.transpile {
   213  			if verbose {
   214  				io.ErrPrintfln("=== PREC  %s", pkg.Dir)
   215  			}
   216  			transpileOpts := newTranspileOptions(&transpileCfg{
   217  				output: tempdirRoot,
   218  			})
   219  			err := transpilePkg(importPath(pkg.Dir), transpileOpts)
   220  			if err != nil {
   221  				io.ErrPrintln(err)
   222  				io.ErrPrintln("FAIL")
   223  				io.ErrPrintfln("FAIL    %s", pkg.Dir)
   224  				io.ErrPrintln("FAIL")
   225  
   226  				buildErrCount++
   227  				continue
   228  			}
   229  
   230  			if verbose {
   231  				io.ErrPrintfln("=== BUILD %s", pkg.Dir)
   232  			}
   233  			tempDir, err := ResolvePath(tempdirRoot, importPath(pkg.Dir))
   234  			if err != nil {
   235  				return errors.New("cannot resolve build dir")
   236  			}
   237  			err = goBuildFileOrPkg(tempDir, defaultTranspileCfg)
   238  			if err != nil {
   239  				io.ErrPrintln(err)
   240  				io.ErrPrintln("FAIL")
   241  				io.ErrPrintfln("FAIL    %s", pkg.Dir)
   242  				io.ErrPrintln("FAIL")
   243  
   244  				buildErrCount++
   245  				continue
   246  			}
   247  		}
   248  
   249  		if len(pkg.TestGnoFiles) == 0 && len(pkg.FiletestGnoFiles) == 0 {
   250  			io.ErrPrintfln("?       %s \t[no test files]", pkg.Dir)
   251  			continue
   252  		}
   253  
   254  		sort.Strings(pkg.TestGnoFiles)
   255  		sort.Strings(pkg.FiletestGnoFiles)
   256  
   257  		startedAt := time.Now()
   258  		err = gnoTestPkg(pkg.Dir, pkg.TestGnoFiles, pkg.FiletestGnoFiles, cfg, io)
   259  		duration := time.Since(startedAt)
   260  		dstr := fmtDuration(duration)
   261  
   262  		if err != nil {
   263  			io.ErrPrintfln("%s: test pkg: %v", pkg.Dir, err)
   264  			io.ErrPrintfln("FAIL")
   265  			io.ErrPrintfln("FAIL    %s \t%s", pkg.Dir, dstr)
   266  			io.ErrPrintfln("FAIL")
   267  			testErrCount++
   268  		} else {
   269  			io.ErrPrintfln("ok      %s \t%s", pkg.Dir, dstr)
   270  		}
   271  	}
   272  	if testErrCount > 0 || buildErrCount > 0 {
   273  		io.ErrPrintfln("FAIL")
   274  		return fmt.Errorf("FAIL: %d build errors, %d test errors", buildErrCount, testErrCount)
   275  	}
   276  
   277  	return nil
   278  }
   279  
   280  func gnoTestPkg(
   281  	pkgPath string,
   282  	unittestFiles,
   283  	filetestFiles []string,
   284  	cfg *testCfg,
   285  	io commands.IO,
   286  ) error {
   287  	var (
   288  		verbose             = cfg.verbose
   289  		rootDir             = cfg.rootDir
   290  		runFlag             = cfg.run
   291  		printRuntimeMetrics = cfg.printRuntimeMetrics
   292  
   293  		stdin  = io.In()
   294  		stdout = io.Out()
   295  		stderr = io.Err()
   296  		errs   error
   297  	)
   298  
   299  	mode := tests.ImportModeStdlibsOnly
   300  	if cfg.withNativeFallback {
   301  		// XXX: display a warn?
   302  		mode = tests.ImportModeStdlibsPreferred
   303  	}
   304  	if !verbose {
   305  		// TODO: speedup by ignoring if filter is file/*?
   306  		mockOut := bytes.NewBufferString("")
   307  		stdout = commands.WriteNopCloser(mockOut)
   308  	}
   309  
   310  	// testing with *_test.gno
   311  	if len(unittestFiles) > 0 {
   312  		// Determine gnoPkgPath by reading gno.mod
   313  		var gnoPkgPath string
   314  		modfile, err := gnomod.ParseAt(pkgPath)
   315  		if err == nil {
   316  			gnoPkgPath = modfile.Module.Mod.Path
   317  		} else {
   318  			gnoPkgPath = pkgPathFromRootDir(pkgPath, rootDir)
   319  			if gnoPkgPath == "" {
   320  				// unable to read pkgPath from gno.mod, generate a random realm path
   321  				io.ErrPrintfln("--- WARNING: unable to read package path from gno.mod or gno root directory; try creating a gno.mod file")
   322  				gnoPkgPath = transpiler.GnoRealmPkgsPrefixBefore + random.RandStr(8)
   323  			}
   324  		}
   325  		memPkg := gno.ReadMemPackage(pkgPath, gnoPkgPath)
   326  
   327  		// tfiles, ifiles := gno.ParseMemPackageTests(memPkg)
   328  		tfiles, ifiles := parseMemPackageTests(memPkg)
   329  		testPkgName := getPkgNameFromFileset(ifiles)
   330  
   331  		// run test files in pkg
   332  		if len(tfiles.Files) > 0 {
   333  			testStore := tests.TestStore(
   334  				rootDir, "",
   335  				stdin, stdout, stderr,
   336  				mode,
   337  			)
   338  			if verbose {
   339  				testStore.SetLogStoreOps(true)
   340  			}
   341  
   342  			m := tests.TestMachine(testStore, stdout, gnoPkgPath)
   343  			if printRuntimeMetrics {
   344  				// from tm2/pkg/sdk/vm/keeper.go
   345  				// XXX: make maxAllocTx configurable.
   346  				maxAllocTx := int64(500 * 1000 * 1000)
   347  
   348  				m.Alloc = gno.NewAllocator(maxAllocTx)
   349  			}
   350  			m.RunMemPackage(memPkg, true)
   351  			err := runTestFiles(m, tfiles, memPkg.Name, verbose, printRuntimeMetrics, runFlag, io)
   352  			if err != nil {
   353  				errs = multierr.Append(errs, err)
   354  			}
   355  		}
   356  
   357  		// test xxx_test pkg
   358  		if len(ifiles.Files) > 0 {
   359  			testStore := tests.TestStore(
   360  				rootDir, "",
   361  				stdin, stdout, stderr,
   362  				mode,
   363  			)
   364  			if verbose {
   365  				testStore.SetLogStoreOps(true)
   366  			}
   367  
   368  			m := tests.TestMachine(testStore, stdout, testPkgName)
   369  
   370  			memFiles := make([]*std.MemFile, 0, len(ifiles.FileNames())+1)
   371  			for _, f := range memPkg.Files {
   372  				for _, ifileName := range ifiles.FileNames() {
   373  					if f.Name == "gno.mod" || f.Name == ifileName {
   374  						memFiles = append(memFiles, f)
   375  						break
   376  					}
   377  				}
   378  			}
   379  
   380  			memPkg.Files = memFiles
   381  			memPkg.Name = testPkgName
   382  			memPkg.Path = memPkg.Path + "_test"
   383  			m.RunMemPackage(memPkg, true)
   384  
   385  			err := runTestFiles(m, ifiles, testPkgName, verbose, printRuntimeMetrics, runFlag, io)
   386  			if err != nil {
   387  				errs = multierr.Append(errs, err)
   388  			}
   389  		}
   390  	}
   391  
   392  	// testing with *_filetest.gno
   393  	{
   394  		filter := splitRegexp(runFlag)
   395  		for _, testFile := range filetestFiles {
   396  			testFileName := filepath.Base(testFile)
   397  			testName := "file/" + testFileName
   398  			if !shouldRun(filter, testName) {
   399  				continue
   400  			}
   401  
   402  			startedAt := time.Now()
   403  			if verbose {
   404  				io.ErrPrintfln("=== RUN   %s", testName)
   405  			}
   406  
   407  			var closer func() (string, error)
   408  			if !verbose {
   409  				closer = testutils.CaptureStdoutAndStderr()
   410  			}
   411  
   412  			testFilePath := filepath.Join(pkgPath, testFileName)
   413  			err := tests.RunFileTest(rootDir, testFilePath, tests.WithSyncWanted(cfg.updateGoldenTests))
   414  			duration := time.Since(startedAt)
   415  			dstr := fmtDuration(duration)
   416  
   417  			if err != nil {
   418  				errs = multierr.Append(errs, err)
   419  				io.ErrPrintfln("--- FAIL: %s (%s)", testName, dstr)
   420  				if verbose {
   421  					stdouterr, err := closer()
   422  					if err != nil {
   423  						panic(err)
   424  					}
   425  					fmt.Fprintln(os.Stderr, stdouterr)
   426  				}
   427  				continue
   428  			}
   429  
   430  			if verbose {
   431  				io.ErrPrintfln("--- PASS: %s (%s)", testName, dstr)
   432  			}
   433  			// XXX: add per-test metrics
   434  		}
   435  	}
   436  
   437  	return errs
   438  }
   439  
   440  // attempts to determine the full gno pkg path by analyzing the directory.
   441  func pkgPathFromRootDir(pkgPath, rootDir string) string {
   442  	abPkgPath, err := filepath.Abs(pkgPath)
   443  	if err != nil {
   444  		log.Printf("could not determine abs path: %v", err)
   445  		return ""
   446  	}
   447  	abRootDir, err := filepath.Abs(rootDir)
   448  	if err != nil {
   449  		log.Printf("could not determine abs path: %v", err)
   450  		return ""
   451  	}
   452  	abRootDir += string(filepath.Separator)
   453  	if !strings.HasPrefix(abPkgPath, abRootDir) {
   454  		return ""
   455  	}
   456  	impPath := strings.ReplaceAll(abPkgPath[len(abRootDir):], string(filepath.Separator), "/")
   457  	for _, prefix := range [...]string{
   458  		"examples/",
   459  		"gnovm/stdlibs/",
   460  		"gnovm/tests/stdlibs/",
   461  	} {
   462  		if strings.HasPrefix(impPath, prefix) {
   463  			return impPath[len(prefix):]
   464  		}
   465  	}
   466  	return ""
   467  }
   468  
   469  func runTestFiles(
   470  	m *gno.Machine,
   471  	files *gno.FileSet,
   472  	pkgName string,
   473  	verbose bool,
   474  	printRuntimeMetrics bool,
   475  	runFlag string,
   476  	io commands.IO,
   477  ) (errs error) {
   478  	defer func() {
   479  		if r := recover(); r != nil {
   480  			errs = multierr.Append(fmt.Errorf("panic: %v\nstack:\n%v\ngno machine: %v", r, string(debug.Stack()), m.String()), errs)
   481  		}
   482  	}()
   483  
   484  	testFuncs := &testFuncs{
   485  		PackageName: pkgName,
   486  		Verbose:     verbose,
   487  		RunFlag:     runFlag,
   488  	}
   489  	loadTestFuncs(pkgName, testFuncs, files)
   490  
   491  	// before/after statistics
   492  	numPackagesBefore := m.Store.NumMemPackages()
   493  
   494  	testmain, err := formatTestmain(testFuncs)
   495  	if err != nil {
   496  		log.Fatal(err)
   497  	}
   498  
   499  	m.RunFiles(files.Files...)
   500  	n := gno.MustParseFile("main_test.gno", testmain)
   501  	m.RunFiles(n)
   502  
   503  	for _, test := range testFuncs.Tests {
   504  		testFuncStr := fmt.Sprintf("%q", test.Name)
   505  
   506  		eval := m.Eval(gno.Call("runtest", testFuncStr))
   507  
   508  		ret := eval[0].GetString()
   509  		if ret == "" {
   510  			err := errors.New("failed to execute unit test: %q", test.Name)
   511  			errs = multierr.Append(errs, err)
   512  			io.ErrPrintfln("--- FAIL: %s [internal gno testing error]", test.Name)
   513  			continue
   514  		}
   515  
   516  		// TODO: replace with amino or send native type?
   517  		var rep report
   518  		err = json.Unmarshal([]byte(ret), &rep)
   519  		if err != nil {
   520  			errs = multierr.Append(errs, err)
   521  			io.ErrPrintfln("--- FAIL: %s [internal gno testing error]", test.Name)
   522  			continue
   523  		}
   524  
   525  		if rep.Failed {
   526  			err := errors.New("failed: %q", test.Name)
   527  			errs = multierr.Append(errs, err)
   528  		}
   529  
   530  		if printRuntimeMetrics {
   531  			imports := m.Store.NumMemPackages() - numPackagesBefore - 1
   532  			// XXX: store changes
   533  			// XXX: max mem consumption
   534  			allocsVal := "n/a"
   535  			if m.Alloc != nil {
   536  				maxAllocs, allocs := m.Alloc.Status()
   537  				allocsVal = fmt.Sprintf("%s(%.2f%%)",
   538  					prettySize(allocs),
   539  					float64(allocs)/float64(maxAllocs)*100,
   540  				)
   541  			}
   542  			io.ErrPrintfln("---       runtime: cycle=%s imports=%d allocs=%s",
   543  				prettySize(m.Cycles),
   544  				imports,
   545  				allocsVal,
   546  			)
   547  		}
   548  	}
   549  
   550  	return errs
   551  }
   552  
   553  // mirror of stdlibs/testing.Report
   554  type report struct {
   555  	Failed  bool
   556  	Skipped bool
   557  }
   558  
   559  var testmainTmpl = template.Must(template.New("testmain").Parse(`
   560  package {{ .PackageName }}
   561  
   562  import (
   563  	"testing"
   564  )
   565  
   566  var tests = []testing.InternalTest{
   567  {{range .Tests}}
   568      {"{{.Name}}", {{.Name}}},
   569  {{end}}
   570  }
   571  
   572  func runtest(name string) (report string) {
   573  	for _, test := range tests {
   574  		if test.Name == name {
   575  			return testing.RunTest({{printf "%q" .RunFlag}}, {{.Verbose}}, test)
   576  		}
   577  	}
   578  	panic("no such test: " + name)
   579  	return ""
   580  }
   581  `))
   582  
   583  type testFuncs struct {
   584  	Tests       []testFunc
   585  	PackageName string
   586  	Verbose     bool
   587  	RunFlag     string
   588  }
   589  
   590  type testFunc struct {
   591  	Package string
   592  	Name    string
   593  }
   594  
   595  func getPkgNameFromFileset(files *gno.FileSet) string {
   596  	if len(files.Files) <= 0 {
   597  		return ""
   598  	}
   599  	return string(files.Files[0].PkgName)
   600  }
   601  
   602  func formatTestmain(t *testFuncs) (string, error) {
   603  	var buf bytes.Buffer
   604  	if err := testmainTmpl.Execute(&buf, t); err != nil {
   605  		return "", err
   606  	}
   607  	return buf.String(), nil
   608  }
   609  
   610  func loadTestFuncs(pkgName string, t *testFuncs, tfiles *gno.FileSet) *testFuncs {
   611  	for _, tf := range tfiles.Files {
   612  		for _, d := range tf.Decls {
   613  			if fd, ok := d.(*gno.FuncDecl); ok {
   614  				fname := string(fd.Name)
   615  				if strings.HasPrefix(fname, "Test") {
   616  					tf := testFunc{
   617  						Package: pkgName,
   618  						Name:    fname,
   619  					}
   620  					t.Tests = append(t.Tests, tf)
   621  				}
   622  			}
   623  		}
   624  	}
   625  	return t
   626  }
   627  
   628  // parseMemPackageTests is copied from gno.ParseMemPackageTests
   629  // for except to _filetest.gno
   630  func parseMemPackageTests(memPkg *std.MemPackage) (tset, itset *gno.FileSet) {
   631  	tset = &gno.FileSet{}
   632  	itset = &gno.FileSet{}
   633  	for _, mfile := range memPkg.Files {
   634  		if !strings.HasSuffix(mfile.Name, ".gno") {
   635  			continue // skip this file.
   636  		}
   637  		if strings.HasSuffix(mfile.Name, "_filetest.gno") {
   638  			continue
   639  		}
   640  		n, err := gno.ParseFile(mfile.Name, mfile.Body)
   641  		if err != nil {
   642  			panic(errors.Wrap(err, "parsing file "+mfile.Name))
   643  		}
   644  		if n == nil {
   645  			panic("should not happen")
   646  		}
   647  		if strings.HasSuffix(mfile.Name, "_test.gno") {
   648  			// add test file.
   649  			if memPkg.Name+"_test" == string(n.PkgName) {
   650  				itset.AddFiles(n)
   651  			} else {
   652  				tset.AddFiles(n)
   653  			}
   654  		} else if memPkg.Name == string(n.PkgName) {
   655  			// skip package file.
   656  		} else {
   657  			panic(fmt.Sprintf(
   658  				"expected package name [%s] or [%s_test] but got [%s] file [%s]",
   659  				memPkg.Name, memPkg.Name, n.PkgName, mfile))
   660  		}
   661  	}
   662  	return tset, itset
   663  }
   664  
   665  func shouldRun(filter filterMatch, path string) bool {
   666  	if filter == nil {
   667  		return true
   668  	}
   669  	elem := strings.Split(path, "/")
   670  	ok, _ := filter.matches(elem, matchString)
   671  	return ok
   672  }