From a2971bf011b2239cc4d368f586cfe9650c538ea7 Mon Sep 17 00:00:00 2001 From: Hariom Verma Date: Thu, 12 Oct 2023 22:09:52 +0530 Subject: [PATCH] feat: `gno test` support `/...` pattern (#1078) BREAKING CHANGE: Altered behavior of the `gno test` command Adds support for `/...` pattern in `gno test` command. Now args can have `/...` pattern in the directory path. Using `gno test ./path/to/pkg` would trigger the execution of test files solely within the specified package directory, excluding any subdirectories like `./path/to/pkg/subpkg`. To execute test files within subdirectories as well, use `gno test ./path/to/pkg/...` It supports all variations of `/...` such as `./path/.../pkg`, `./.../pkg`, ,`./.../path/...` and more
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--- .github/workflows/examples.yml | 2 +- examples/Makefile | 4 +- gnovm/cmd/gno/test.go | 6 +- .../cmd/gno/testdata/gno_test/empty_dir.txtar | 5 + .../gno/testdata/gno_test/empty_gno1.txtar | 2 +- .../gno/testdata/gno_test/error_correct.txtar | 2 +- .../testdata/gno_test/failing_filetest.txtar | 4 +- gnovm/cmd/gno/testdata/gno_test/minim1.txtar | 2 +- gnovm/cmd/gno/testdata/gno_test/minim2.txtar | 2 +- gnovm/cmd/gno/testdata/gno_test/minim3.txtar | 2 +- .../testdata/gno_test/output_correct.txtar | 2 +- .../gno/testdata/gno_test/output_sync.txtar | 2 +- .../gno/testdata/gno_test/realm_correct.txtar | 2 +- .../gno/testdata/gno_test/realm_sync.txtar | 2 +- .../testdata/gno_test/valid_filetest.txtar | 4 +- .../gno/testdata/gno_test/valid_test.txtar | 7 +- gnovm/cmd/gno/util.go | 84 +++++ gnovm/cmd/gno/util_test.go | 297 ++++++++++++++++++ 18 files changed, 411 insertions(+), 20 deletions(-) create mode 100644 gnovm/cmd/gno/util_test.go diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index d4b3079d612..b17c66d8e5a 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -50,7 +50,7 @@ jobs: with: go-version: ${{ matrix.goversion }} - run: go install -v ./gnovm/cmd/gno - - run: go run ./gnovm/cmd/gno test --verbose ./examples + - run: go run ./gnovm/cmd/gno test --verbose ./examples/... lint: strategy: fail-fast: false diff --git a/examples/Makefile b/examples/Makefile index 5075df198ac..f20072d9df2 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -19,7 +19,7 @@ build: precompile .PHONY: test test: - go run ../gnovm/cmd/gno test --verbose . + go run ../gnovm/cmd/gno test --verbose ./... .PHONY: lint lint: @@ -27,7 +27,7 @@ lint: .PHONY: test.sync test.sync: - go run ../gnovm/cmd/gno test --verbose --update-golden-tests . + go run ../gnovm/cmd/gno test --verbose --update-golden-tests ./... .PHONY: clean clean: diff --git a/gnovm/cmd/gno/test.go b/gnovm/cmd/gno/test.go index 8cacfd5623b..85fe3f7ee7d 100644 --- a/gnovm/cmd/gno/test.go +++ b/gnovm/cmd/gno/test.go @@ -50,7 +50,7 @@ func newTestCmd(io *commands.IO) *commands.Command { 'gno test' recompiles each package along with any files with names matching the file pattern "*_test.gno" or "*_filetest.gno". -The only supported for now is a directory (relative or absolute). +The can be directory or file path (relative or absolute). - "*_test.gno" files work like "*_test.go" files, but they contain only test functions. Benchmark and fuzz functions aren't supported yet. Similarly, only @@ -182,9 +182,9 @@ func execTest(cfg *testCfg, args []string, io *commands.IO) error { cfg.rootDir = guessRootDir() } - paths, err := gnoPackagesFromArgs(args) + paths, err := targetsFromPatterns(args) if err != nil { - return fmt.Errorf("list package paths from args: %w", err) + return fmt.Errorf("list targets from patterns: %w", err) } if len(paths) == 0 { io.ErrPrintln("no packages to test") diff --git a/gnovm/cmd/gno/testdata/gno_test/empty_dir.txtar b/gnovm/cmd/gno/testdata/gno_test/empty_dir.txtar index 73f0da72dfe..ffed64ab9c7 100644 --- a/gnovm/cmd/gno/testdata/gno_test/empty_dir.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/empty_dir.txtar @@ -2,5 +2,10 @@ gno test . +! stdout .+ +stderr '[no test files]' + +gno test ./... + ! stdout .+ stderr 'no packages to test' diff --git a/gnovm/cmd/gno/testdata/gno_test/empty_gno1.txtar b/gnovm/cmd/gno/testdata/gno_test/empty_gno1.txtar index ae73b3ab275..cc673bb38ff 100644 --- a/gnovm/cmd/gno/testdata/gno_test/empty_gno1.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/empty_gno1.txtar @@ -3,7 +3,7 @@ gno test . ! stdout .+ -stderr '\? \./\. \[no test files\]' +stderr '\? \. \[no test files\]' ! gno test --precompile . diff --git a/gnovm/cmd/gno/testdata/gno_test/error_correct.txtar b/gnovm/cmd/gno/testdata/gno_test/error_correct.txtar index c7d3187424c..a66d831b48c 100644 --- a/gnovm/cmd/gno/testdata/gno_test/error_correct.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/error_correct.txtar @@ -5,7 +5,7 @@ gno test -verbose . stdout 'Machine\.RunMain\(\) panic: oups' stderr '=== RUN file/x_filetest.gno' stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \./\. \d\.\d\ds' +stderr 'ok \. \d\.\d\ds' -- x_filetest.gno -- package main diff --git a/gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar b/gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar index bc3efc1a8c9..c739c1ce328 100644 --- a/gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar @@ -9,8 +9,8 @@ stderr 'panic: fail on failing_filetest.gno: got unexpected error: beep boop' ! gno test -verbose --precompile . stdout 'Machine.RunMain\(\) panic: beep boop' -stderr '=== PREC \./\.' -stderr '=== BUILD \./\.' +stderr '=== PREC \.' +stderr '=== BUILD \.' stderr '=== RUN file/failing_filetest.gno' stderr 'panic: fail on failing_filetest.gno: got unexpected error: beep boop' diff --git a/gnovm/cmd/gno/testdata/gno_test/minim1.txtar b/gnovm/cmd/gno/testdata/gno_test/minim1.txtar index 231ef4a270c..b0a77186086 100644 --- a/gnovm/cmd/gno/testdata/gno_test/minim1.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/minim1.txtar @@ -3,7 +3,7 @@ gno test . ! stdout .+ -stderr '\? \./\. \[no test files\]' +stderr '\? \. \[no test files\]' -- minim.gno -- package minim diff --git a/gnovm/cmd/gno/testdata/gno_test/minim2.txtar b/gnovm/cmd/gno/testdata/gno_test/minim2.txtar index 038dfd19289..3c4d1d085f0 100644 --- a/gnovm/cmd/gno/testdata/gno_test/minim2.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/minim2.txtar @@ -3,7 +3,7 @@ gno test . ! stdout .+ -stderr 'ok \./\. \d\.\d\ds' +stderr 'ok \. \d\.\d\ds' -- minim.gno -- package minim diff --git a/gnovm/cmd/gno/testdata/gno_test/minim3.txtar b/gnovm/cmd/gno/testdata/gno_test/minim3.txtar index 8e657104801..ac8ae0c41d4 100644 --- a/gnovm/cmd/gno/testdata/gno_test/minim3.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/minim3.txtar @@ -3,7 +3,7 @@ gno test . ! stdout .+ -stderr 'ok \./\. \d\.\d\ds' +stderr 'ok \. \d\.\d\ds' -- minim.gno -- package minim diff --git a/gnovm/cmd/gno/testdata/gno_test/output_correct.txtar b/gnovm/cmd/gno/testdata/gno_test/output_correct.txtar index ce12874f669..4e5495ab839 100644 --- a/gnovm/cmd/gno/testdata/gno_test/output_correct.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/output_correct.txtar @@ -5,7 +5,7 @@ gno test -verbose . ! stdout .+ # stdout should be empty stderr '=== RUN file/x_filetest.gno' stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \./\. \d\.\d\ds' +stderr 'ok \. \d\.\d\ds' -- x_filetest.gno -- package main diff --git a/gnovm/cmd/gno/testdata/gno_test/output_sync.txtar b/gnovm/cmd/gno/testdata/gno_test/output_sync.txtar index 85fec4ab316..b21db788924 100644 --- a/gnovm/cmd/gno/testdata/gno_test/output_sync.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/output_sync.txtar @@ -5,7 +5,7 @@ gno test -verbose . -update-golden-tests ! stdout .+ # stdout should be empty stderr '=== RUN file/x_filetest.gno' stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \./\. \d\.\d\ds' +stderr 'ok \. \d\.\d\ds' cmp x_filetest.gno x_filetest.gno.golden diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar b/gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar index f376e61d9a4..c3d3b983e34 100644 --- a/gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar @@ -5,7 +5,7 @@ gno test -verbose . ! stdout .+ # stdout should be empty stderr '=== RUN file/x_filetest.gno' stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \./\. \d\.\d\ds' +stderr 'ok \. \d\.\d\ds' -- x_filetest.gno -- // PKGPATH: gno.land/r/x diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar b/gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar index 918fb0b88c9..236e69f8641 100644 --- a/gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar @@ -5,7 +5,7 @@ gno test -verbose . -update-golden-tests ! stdout .+ # stdout should be empty stderr '=== RUN file/x_filetest.gno' stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \./\. \d\.\d\ds' +stderr 'ok \. \d\.\d\ds' cmp x_filetest.gno x_filetest.gno.golden diff --git a/gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar b/gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar index 51b5f323654..545607a9082 100644 --- a/gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar @@ -3,14 +3,14 @@ gno test . ! stdout .+ -stderr 'ok \./\. \d\.\d\ds' +stderr 'ok \. \d\.\d\ds' gno test -verbose . ! stdout .+ stderr '=== RUN file/valid_filetest.gno' stderr '--- PASS: file/valid_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \./\. \d\.\d\ds' +stderr 'ok \. \d\.\d\ds' -- valid.gno -- package valid diff --git a/gnovm/cmd/gno/testdata/gno_test/valid_test.txtar b/gnovm/cmd/gno/testdata/gno_test/valid_test.txtar index cb5f7286f60..9590626776c 100644 --- a/gnovm/cmd/gno/testdata/gno_test/valid_test.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/valid_test.txtar @@ -3,7 +3,12 @@ gno test . ! stdout .+ -stderr 'ok \./\. \d\.\d\ds' +stderr 'ok \. \d\.\d\ds' + +gno test ./... + +! stdout .+ +stderr 'ok \. \d\.\d\ds' -- valid.gno -- package valid diff --git a/gnovm/cmd/gno/util.go b/gnovm/cmd/gno/util.go index 8288539c97b..73ee0f0323b 100644 --- a/gnovm/cmd/gno/util.go +++ b/gnovm/cmd/gno/util.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "strings" "time" @@ -105,6 +106,89 @@ func gnoPackagesFromArgs(args []string) ([]string, error) { return paths, nil } +// targetsFromPatterns returns a list of target paths that match the patterns. +// Each pattern can represent a file or a directory, and if the pattern +// includes "/...", the "..." is treated as a wildcard, matching any string. +// Intended to be used by gno commands such as `gno test`. +func targetsFromPatterns(patterns []string) ([]string, error) { + paths := []string{} + for _, p := range patterns { + var match func(string) bool + patternLookup := false + dirToSearch := p + + // Check if the pattern includes `/...` + if strings.Contains(p, "/...") { + index := strings.Index(p, "/...") + if index != -1 { + dirToSearch = p[:index] // Extract the directory path to search + } + match = matchPattern(strings.TrimPrefix(p, "./")) + patternLookup = true + } + + info, err := os.Stat(dirToSearch) + if err != nil { + return nil, fmt.Errorf("invalid file or package path: %w", err) + } + + // If the pattern is a file or a directory + // without `/...`, add it to the list. + if !info.IsDir() || !patternLookup { + paths = append(paths, p) + continue + } + + // the pattern is a dir containing `/...`, walk the dir recursively and + // look for directories containing at least one .gno file and match pattern. + visited := map[string]bool{} // used to run the builder only once per folder. + err = filepath.WalkDir(dirToSearch, func(curpath string, f fs.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("%s: walk dir: %w", dirToSearch, err) + } + // Skip directories and non ".gno" files. + if f.IsDir() || !isGnoFile(f) { + return nil + } + + parentDir := filepath.Dir(curpath) + if _, found := visited[parentDir]; found { + return nil + } + + visited[parentDir] = true + if match(parentDir) { + paths = append(paths, parentDir) + } + + return nil + }) + if err != nil { + return nil, err + } + } + return paths, nil +} + +// matchPattern(pattern)(name) reports whether +// name matches pattern. Pattern is a limited glob +// pattern in which '...' means 'any string' and there +// is no other special syntax. +// Simplified version of go source's matchPatternInternal +// (see $GOROOT/src/cmd/internal/pkgpattern) +func matchPattern(pattern string) func(name string) bool { + re := regexp.QuoteMeta(pattern) + re = strings.Replace(re, `\.\.\.`, `.*`, -1) + // Special case: foo/... matches foo too. + if strings.HasSuffix(re, `/.*`) { + re = re[:len(re)-len(`/.*`)] + `(/.*)?` + } + reg := regexp.MustCompile(`^` + re + `$`) + return func(name string) bool { + return reg.MatchString(name) + } +} + func fmtDuration(d time.Duration) string { return fmt.Sprintf("%.2fs", d.Seconds()) } diff --git a/gnovm/cmd/gno/util_test.go b/gnovm/cmd/gno/util_test.go new file mode 100644 index 00000000000..9e9659bfe4f --- /dev/null +++ b/gnovm/cmd/gno/util_test.go @@ -0,0 +1,297 @@ +package main + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMatchPattern(t *testing.T) { + tests := []struct { + pattern string + names []string + expected []bool + }{ + { + pattern: "foo", + names: []string{"foo", "bar", "baz", "foo/bar"}, + expected: []bool{true, false, false, false}, + }, + { + pattern: "foo/...", + names: []string{"foo", "foo/bar", "foo/bar/baz", "bar", "baz"}, + expected: []bool{true, true, true, false, false}, + }, + { + pattern: "foo/bar/...", + names: []string{"foo/bar", "foo/bar/baz", "foo/baz/bar", "foo", "bar"}, + expected: []bool{true, true, false, false, false}, + }, + { + pattern: "foo/.../baz", + names: []string{"foo/bar", "foo/bar/baz", "foo/baz/bar", "foo", "bar"}, + expected: []bool{false, true, false, false, false}, + }, + { + pattern: "foo/.../baz/...", + names: []string{"foo/bar/baz", "foo/baz/bar", "foo/bar/baz/qux", "foo/baz/bar/qux"}, + expected: []bool{true, false, true, false}, + }, + { + pattern: "...", + names: []string{"foo", "bar", "baz", "foo/bar", "foo/bar/baz"}, + expected: []bool{true, true, true, true, true}, + }, + { + pattern: ".../bar", + names: []string{"foo", "bar", "baz", "foo/bar", "foo/bar/baz"}, + expected: []bool{false, false, false, true, false}, + }, + } + + for _, test := range tests { + t.Run(test.pattern, func(t *testing.T) { + matchFunc := matchPattern(test.pattern) + for i, name := range test.names { + res := matchFunc(name) + assert.Equal(t, test.expected[i], res, "Expected: %v, Got: %v", test.expected[i], res) + } + }) + } +} + +func TestTargetsFromPatterns(t *testing.T) { + tmpDir := t.TempDir() + createGnoPackages(t, tmpDir) + + for _, tc := range []struct { + desc string + in, expected []string + errorShouldContain string + }{ + { + desc: "valid1", + in: []string{ + tmpDir, + }, + expected: []string{ + tmpDir, + }, + }, + { + desc: "valid2", + in: []string{ + tmpDir + "/foo", + }, + expected: []string{ + filepath.Join(tmpDir, "foo"), + }, + }, + { + desc: "valid_recursive1", + in: []string{ + tmpDir + "/...", + }, + expected: []string{ + filepath.Join(tmpDir, "foo"), + filepath.Join(tmpDir, "bar"), + filepath.Join(tmpDir, "baz"), + filepath.Join(tmpDir, "foo", "qux"), + filepath.Join(tmpDir, "bar", "quux"), + filepath.Join(tmpDir, "foo", "qux", "corge"), + }, + }, + { + desc: "valid_recursive2", + in: []string{ + tmpDir + "/foo/...", + }, + expected: []string{ + filepath.Join(tmpDir, "foo"), + filepath.Join(tmpDir, "foo", "qux"), + filepath.Join(tmpDir, "foo", "qux", "corge"), + }, + }, + { + desc: "valid_recursive2", + in: []string{ + tmpDir + "/.../qux", + }, + expected: []string{ + filepath.Join(tmpDir, "foo", "qux"), + }, + }, + { + desc: "valid_recursive3", + in: []string{ + tmpDir + "/.../qux/...", + }, + expected: []string{ + filepath.Join(tmpDir, "foo", "qux"), + filepath.Join(tmpDir, "foo", "qux", "corge"), + }, + }, + { + desc: "multiple_input", + in: []string{ + tmpDir + "/foo", + tmpDir + "/bar", + tmpDir + "/baz", + }, + expected: []string{ + filepath.Join(tmpDir, "foo"), + filepath.Join(tmpDir, "bar"), + filepath.Join(tmpDir, "baz"), + }, + }, + { + desc: "mixed_input1", + in: []string{ + tmpDir + "/foo", + tmpDir + "/bar/...", + }, + expected: []string{ + filepath.Join(tmpDir, "foo"), + filepath.Join(tmpDir, "bar"), + filepath.Join(tmpDir, "bar", "quux"), + }, + }, + { + desc: "mixed_input2", + in: []string{ + tmpDir + "/foo", + tmpDir + "/bar/...", + tmpDir + "/baz/baz.gno", + }, + expected: []string{ + filepath.Join(tmpDir, "foo"), + filepath.Join(tmpDir, "bar"), + filepath.Join(tmpDir, "bar", "quux"), + filepath.Join(tmpDir, "baz", "baz.gno"), + }, + }, + { + desc: "not_exists1", + in: []string{ + tmpDir + "/notexists", // dir path + }, + errorShouldContain: "no such file or directory", + }, + { + desc: "not_exists2", + in: []string{ + tmpDir + "/foo/bar.gno", // file path + }, + errorShouldContain: "no such file or directory", + }, + { + desc: "not_exists3", // mixed + in: []string{ + tmpDir + "/foo", // exists + tmpDir + "/notexists", // not exists + }, + errorShouldContain: "no such file or directory", + }, + } { + t.Run(tc.desc, func(t *testing.T) { + targets, err := targetsFromPatterns(tc.in) + if tc.errorShouldContain != "" { + assert.ErrorContains(t, err, tc.errorShouldContain) + return + } + assert.NoError(t, err) + require.Equal(t, len(tc.expected), len(targets)) + for _, tr := range targets { + assert.Contains(t, tc.expected, tr) + } + }) + } +} + +func createGnoPackages(t *testing.T, tmpDir string) { + t.Helper() + + type file struct { + name, data string + } + // Gno pkgs to create + pkgs := []struct { + dir string + files []file + }{ + // pkg 'foo', 'bar' and 'baz' + { + dir: filepath.Join(tmpDir, "foo"), + files: []file{ + { + name: "foo.gno", + data: `package foo`, + }, + }, + }, + { + dir: filepath.Join(tmpDir, "bar"), + files: []file{ + { + name: "bar.gno", + data: `package bar`, + }, + }, + }, + { + dir: filepath.Join(tmpDir, "baz"), + files: []file{ + { + name: "baz.gno", + data: `package baz`, + }, + }, + }, + + // pkg inside 'foo' pkg + { + dir: filepath.Join(tmpDir, "foo", "qux"), + files: []file{ + { + name: "qux.gno", + data: `package qux`, + }, + }, + }, + + // pkg inside 'bar' pkg + { + dir: filepath.Join(tmpDir, "bar", "quux"), + files: []file{ + { + name: "quux.gno", + data: `package quux`, + }, + }, + }, + + // pkg inside 'foo/qux' pkg + { + dir: filepath.Join(tmpDir, "foo", "qux", "corge"), + files: []file{ + { + name: "corge.gno", + data: `package corge`, + }, + }, + }, + } + + // Create pkgs + for _, p := range pkgs { + err := os.MkdirAll(p.dir, 0o700) + require.NoError(t, err) + for _, f := range p.files { + err = os.WriteFile(filepath.Join(p.dir, f.name), []byte(f.data), 0o644) + require.NoError(t, err) + } + } +}