diff --git a/gnovm/cmd/gno/doctest.go b/gnovm/cmd/gno/doctest.go new file mode 100644 index 00000000000..7ffba92714f --- /dev/null +++ b/gnovm/cmd/gno/doctest.go @@ -0,0 +1,107 @@ +package main + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "time" + + dt "github.com/gnolang/gno/gnovm/pkg/doctest" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type doctestCfg struct { + markdownPath string + runPattern string + timeout time.Duration +} + +func newDoctestCmd(io commands.IO) *commands.Command { + cfg := &doctestCfg{} + + return commands.NewCommand( + commands.Metadata{ + Name: "doctest", + ShortUsage: "doctest -path [-run ] [-timeout ]", + ShortHelp: "executes a specific code block from a markdown file", + }, + cfg, + func(_ context.Context, args []string) error { + return execDoctest(cfg, args, io) + }, + ) +} + +func (c *doctestCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.markdownPath, + "path", + "", + "path to the markdown file", + ) + fs.StringVar( + &c.runPattern, + "run", + "", + "pattern to match code block names", + ) + fs.DurationVar( + &c.timeout, + "timeout", + time.Second*30, + "timeout for code execution (e.g., 30s, 1m)", + ) +} + +func execDoctest(cfg *doctestCfg, _ []string, io commands.IO) error { + if cfg.markdownPath == "" { + return fmt.Errorf("markdown file path is required") + } + + content, err := fetchMarkdown(cfg.markdownPath) + if err != nil { + return fmt.Errorf("failed to read markdown file: %w", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), cfg.timeout) + defer cancel() + + resultChan := make(chan []string) + errChan := make(chan error) + + go func() { + results, err := dt.ExecuteMatchingCodeBlock(ctx, content, cfg.runPattern) + if err != nil { + errChan <- err + } else { + resultChan <- results + } + }() + + select { + case results := <-resultChan: + if len(results) == 0 { + io.Println("No code blocks matched the pattern") + return nil + } + io.Println("Execution Result:") + io.Println(strings.Join(results, "\n\n")) + case err := <-errChan: + return fmt.Errorf("failed to execute code block: %w", err) + case <-ctx.Done(): + return fmt.Errorf("execution timed out after %v", cfg.timeout) + } + + return nil +} + +// fetchMarkdown reads a markdown file and returns its content +func fetchMarkdown(path string) (string, error) { + content, err := os.ReadFile(path) + if err != nil { + return "", fmt.Errorf("failed to read file: %w", err) + } + return string(content), nil +} diff --git a/gnovm/cmd/gno/doctest_test.go b/gnovm/cmd/gno/doctest_test.go new file mode 100644 index 00000000000..9ce9fc8942e --- /dev/null +++ b/gnovm/cmd/gno/doctest_test.go @@ -0,0 +1,97 @@ +package main + +import ( + "os" + "testing" +) + +func TestDoctest(t *testing.T) { + tempDir, err := os.MkdirTemp("", "doctest-test") + if err != nil { + t.Fatalf("failed to create temp directory: %v", err) + } + defer os.RemoveAll(tempDir) + + markdownContent := `# Go Code Examples + +This document contains two simple examples written in Go. + +## Example 1: Fibonacci Sequence + +The first example prints the first 10 numbers of the Fibonacci sequence. + +` + "```go" + ` +// @test: Fibonacci +package main + +func main() { + a, b := 0, 1 + for i := 0; i < 10; i++ { + println(a) + a, b = b, a+b + } +} +` + "```" + ` + +## Example 2: String Reversal + +The second example reverses a given string and prints it. + +` + "```go" + ` +// @test: StringReversal +package main + +func main() { + str := "Hello, Go!" + runes := []rune(str) + for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { + runes[i], runes[j] = runes[j], runes[i] + } + println(string(runes)) +} +` + "```" + ` + +These two examples demonstrate basic Go functionality without using concurrency, generics, or reflect. + +` + "## std Package" + ` +` + "```go" + ` +// @test: StdPackage +package main + +import ( + "std" +) + +func main() { + addr := std.GetOrigCaller() + println(addr) +} +` + "```" + ` +` + + mdFile, err := os.CreateTemp(tempDir, "sample-*.md") + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + defer mdFile.Close() + + _, err = mdFile.WriteString(markdownContent) + if err != nil { + t.Fatalf("failed to write to temp file: %v", err) + } + + mdFilePath := mdFile.Name() + + tc := []testMainCase{ + { + args: []string{"doctest", "-path", mdFilePath, "-run", "StringReversal"}, + stdoutShouldContain: "=== StringReversal ===\n\n!oG ,olleH", + }, + { + args: []string{"doctest", "-path", mdFilePath, "-run", "StdPackage"}, + stdoutShouldContain: "=== StdPackage ===\n\ng14ch5q26mhx3jk5cxl88t278nper264ces4m8nt", + }, + } + + testMainCaseRun(t, tc) +} diff --git a/gnovm/cmd/gno/main.go b/gnovm/cmd/gno/main.go index 7a5799f2835..bd3ef575048 100644 --- a/gnovm/cmd/gno/main.go +++ b/gnovm/cmd/gno/main.go @@ -34,6 +34,7 @@ func newGnocliCmd(io commands.IO) *commands.Command { newDocCmd(io), newEnvCmd(io), newBugCmd(io), + newDoctestCmd(io), newFmtCmd(io), // graph // vendor -- download deps from the chain in vendor/ diff --git a/gnovm/pkg/doctest/README.md b/gnovm/pkg/doctest/README.md new file mode 100644 index 00000000000..901f2d19ce4 --- /dev/null +++ b/gnovm/pkg/doctest/README.md @@ -0,0 +1,87 @@ +# Gno Doctest: Easy Code Execution and Testing + +Gno Doctest is a tool that allows you to easily execute and test code blocks written in the Gno language. This tool offers a range of features, from simple code execution to complex package imports. + +## Basic Usage + +To use Gno Doctest, run the following command: + +gno doctest -path -run + +- ``: Path to the markdown file containing Gno code blocks +- ``: Name of the code block to run (optional) + +For example, to run the code block named "print hello world" in the file "foo.md", use the following command: + +gno doctest -path foo.md -run "print hello world" + +## Features + +### 1. Basic Code Execution + +Gno Doctest can execute simple code blocks: + +```go +package main + +func main() { + println("Hello, World!") +} + +// Output: +// Hello, World! +``` + +Doctest also recognizes that a block of code is a gno. The code below outputs the same result as the example above. + +```go +// @test: print hello world +package main + +func main() { + println("Hello, World!") +} + +// Output: +// Hello, World! +``` + +Running this code will output "Hello, World!". + +### 3. Execution Options + +Doctest supports special execution options: +Ignore Option +Use the ignore tag to skip execution of a code block: + +**Ignore Option** + +Use the ignore tag to skip execution of a code block: + +```go,ignore +// @ignore +package main + +func main() { + println("This won't be executed") +} +``` + +## Conclusion + +Gno Doctest simplifies the process of executing and testing Gno code snippets. + +```go +// @test: slice +package main + +type ints []int + +func main() { + a := ints{1,2,3} + println(a) +} + +// Output: +// (slice[(1 int),(2 int),(3 int)] gno.land/r/g14ch5q26mhx3jk5cxl88t278nper264ces4m8nt/run.ints) +``` diff --git a/gnovm/pkg/doctest/cache.go b/gnovm/pkg/doctest/cache.go new file mode 100644 index 00000000000..7d36228b296 --- /dev/null +++ b/gnovm/pkg/doctest/cache.go @@ -0,0 +1,51 @@ +package doctest + +import ( + "container/list" +) + +const maxCacheSize = 25 + +type cacheItem struct { + key string + value string +} + +type lruCache struct { + capacity int + items map[string]*list.Element + order *list.List +} + +func newCache(capacity int) *lruCache { + return &lruCache{ + capacity: capacity, + items: make(map[string]*list.Element), + order: list.New(), + } +} + +func (c *lruCache) get(key string) (string, bool) { + if elem, ok := c.items[key]; ok { + c.order.MoveToFront(elem) + return elem.Value.(cacheItem).value, true + } + return "", false +} + +func (c *lruCache) set(key, value string) { + if elem, ok := c.items[key]; ok { + c.order.MoveToFront(elem) + elem.Value = cacheItem{key, value} + } else { + if c.order.Len() >= c.capacity { + oldest := c.order.Back() + if oldest != nil { + delete(c.items, oldest.Value.(cacheItem).key) + c.order.Remove(oldest) + } + } + elem := c.order.PushFront(cacheItem{key, value}) + c.items[key] = elem + } +} diff --git a/gnovm/pkg/doctest/exec.go b/gnovm/pkg/doctest/exec.go new file mode 100644 index 00000000000..3b0638d52ed --- /dev/null +++ b/gnovm/pkg/doctest/exec.go @@ -0,0 +1,252 @@ +package doctest + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "path/filepath" + "regexp" + "runtime" + "strings" + + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/db/memdb" + "github.com/gnolang/gno/tm2/pkg/log" + "github.com/gnolang/gno/tm2/pkg/sdk" + authm "github.com/gnolang/gno/tm2/pkg/sdk/auth" + bankm "github.com/gnolang/gno/tm2/pkg/sdk/bank" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/gnolang/gno/tm2/pkg/store" + "github.com/gnolang/gno/tm2/pkg/store/dbadapter" + "github.com/gnolang/gno/tm2/pkg/store/iavl" +) + +// Option constants +const ( + IGNORE = "ignore" // Do not run the code block + SHOULD_PANIC = "should_panic" // Expect a panic + ASSERT = "assert" // Assert the result and expected output are equal +) + +const ( + goLang = "go" + gnoLang = "gno" +) + +// GetStdlibsDir returns the path to the standard libraries directory. +func GetStdlibsDir() string { + _, filename, _, ok := runtime.Caller(0) + if !ok { + panic("cannot get current file path") + } + return filepath.Join(filepath.Dir(filename), "..", "..", "stdlibs") +} + +// cache stores the results of code execution. +var cache = newCache(maxCacheSize) + +// hashCodeBlock generates a SHA256 hash for the given code block. +func hashCodeBlock(c codeBlock) string { + h := sha256.New() + h.Write([]byte(c.content)) + return hex.EncodeToString(h.Sum(nil)) +} + +// ExecuteCodeBlock executes a parsed code block and executes it in a gno VM. +func ExecuteCodeBlock(c codeBlock, stdlibDir string) (string, error) { + if c.options.Ignore { + return "IGNORED", nil + } + + // Extract the actual language from the lang field + lang := strings.Split(c.lang, ",")[0] + + if lang != goLang && lang != gnoLang { + return fmt.Sprintf("SKIPPED (Unsupported language: %s)", lang), nil + } + + if lang == goLang { + lang = gnoLang + } + + hashKey := hashCodeBlock(c) + + // get the result from the cache if it exists + if result, found := cache.get(hashKey); found { + res := strings.TrimSpace(result) + + if c.expectedOutput == "" && c.expectedError == "" { + return fmt.Sprintf("%s (cached)", res), nil + } + + res, err := compareResults(res, c.expectedOutput, c.expectedError) + if err != nil { + return "", err + } + + return fmt.Sprintf("%s (cached)", res), nil + } + + baseKey := store.NewStoreKey("baseKey") + iavlKey := store.NewStoreKey("iavlKey") + + db := memdb.NewMemDB() + + ms := store.NewCommitMultiStore(db) + ms.MountStoreWithDB(baseKey, dbadapter.StoreConstructor, db) + ms.MountStoreWithDB(iavlKey, iavl.StoreConstructor, db) + ms.LoadLatestVersion() + + ctx := sdk.NewContext(sdk.RunTxModeDeliver, ms, &bft.Header{ChainID: "test-chain-id"}, log.NewNoopLogger()) + acck := authm.NewAccountKeeper(iavlKey, std.ProtoBaseAccount) + bank := bankm.NewBankKeeper(acck) + stdlibsDir := GetStdlibsDir() + vmk := vm.NewVMKeeper(baseKey, iavlKey, acck, bank, stdlibsDir, 100_000_000) + + mcw := ms.MultiCacheWrap() + vmk.Initialize(log.NewNoopLogger(), mcw, true) + mcw.MultiWrite() + + files := []*std.MemFile{ + {Name: fmt.Sprintf("%d.%s", c.index, lang), Body: c.content}, + } + + addr := crypto.AddressFromPreimage([]byte("addr1")) + acc := acck.NewAccountWithAddress(ctx, addr) + acck.SetAccount(ctx, acc) + + msg2 := vm.NewMsgRun(addr, std.Coins{}, files) + + res, err := vmk.Run(ctx, msg2) + if c.options.PanicMessage != "" { + if err == nil { + return "", fmt.Errorf("expected panic with message: %s, but executed successfully", c.options.PanicMessage) + } + if !strings.Contains(err.Error(), c.options.PanicMessage) { + return "", fmt.Errorf("expected panic with message: %s, but got: %s", c.options.PanicMessage, err.Error()) + } + return fmt.Sprintf("panicked as expected: %v", err), nil + } + + if err != nil { + return "", err + } + + cache.set(hashKey, res) + + // If there is no expected output or error, It is considered + // a simple code execution and the result is returned as is. + if c.expectedOutput == "" && c.expectedError == "" { + return res, nil + } + + // Otherwise, compare the actual output with the expected output or error. + return compareResults(res, c.expectedOutput, c.expectedError) +} + +// compareResults compares the actual output of code execution with the expected output or error. +func compareResults(actual, expectedOutput, expectedError string) (string, error) { + actual = strings.TrimSpace(actual) + expected := strings.TrimSpace(expectedOutput) + if expected == "" { + expected = strings.TrimSpace(expectedError) + } + + if expected == "" { + if actual != "" { + return "", fmt.Errorf("expected no output, but got:\n%s", actual) + } + return "", nil + } + + if strings.HasPrefix(expected, "regex:") { + return compareRegex(actual, strings.TrimPrefix(expected, "regex:")) + } + + if actual != expected { + return "", fmt.Errorf("expected:\n%s\n\nbut got:\n%s", expected, actual) + } + + return actual, nil +} + +// compareRegex compares the actual output against a regex pattern. +// It returns an error if the regex is invalid or if the actual output does not match the pattern. +func compareRegex(actual, pattern string) (string, error) { + re, err := regexp.Compile(pattern) + if err != nil { + return "", fmt.Errorf("invalid regex pattern: %w", err) + } + + if !re.MatchString(actual) { + return "", fmt.Errorf("output did not match regex pattern:\npattern: %s\nactual: %s", pattern, actual) + } + + return actual, nil +} + +// ExecuteMatchingCodeBlock executes all code blocks in the given content that match the given pattern. +// It returns a slice of execution results as strings and any error encountered during the execution. +func ExecuteMatchingCodeBlock(ctx context.Context, content string, pattern string) ([]string, error) { + codeBlocks := GetCodeBlocks(content) + var results []string + + for _, block := range codeBlocks { + if err := ctx.Err(); err != nil { + return nil, err + } + + if matchPattern(block.name, pattern) { + result, err := ExecuteCodeBlock(block, GetStdlibsDir()) + if err != nil { + return nil, fmt.Errorf("failed to execute code block %s: %w", block.name, err) + } + results = append(results, fmt.Sprintf("\n=== %s ===\n\n%s\n", block.name, result)) + } + } + + return results, nil +} + +var regexCache = make(map[string]*regexp.Regexp) + +// getCompiledRegex retrieves or compiles a regex pattern. +// it uses a cache to store compiled regex patterns for reuse. +func getCompiledRegex(pattern string) (*regexp.Regexp, error) { + re, exists := regexCache[pattern] + if exists { + return re, nil + } + + // double-check in case another goroutine has compiled the regex + if re, exists = regexCache[pattern]; exists { + return re, nil + } + + compiledPattern := regexp.QuoteMeta(pattern) // Escape all regex meta characters + compiledPattern = strings.ReplaceAll(compiledPattern, "\\*", ".*") // Replace escaped `*` with `.*` to match any character + re, err := regexp.Compile(compiledPattern) // Compile the converted pattern + if err != nil { + return nil, err + } + + regexCache[pattern] = re + return re, nil +} + +// matchPattern checks if a name matches the specific pattern. +func matchPattern(name, pattern string) bool { + if pattern == "" { + return true + } + + re, err := getCompiledRegex(pattern) + if err != nil { + return false + } + + return re.MatchString(name) +} diff --git a/gnovm/pkg/doctest/exec_test.go b/gnovm/pkg/doctest/exec_test.go new file mode 100644 index 00000000000..5aeebd452e6 --- /dev/null +++ b/gnovm/pkg/doctest/exec_test.go @@ -0,0 +1,462 @@ +package doctest + +import ( + "context" + "reflect" + "strings" + "testing" + "time" +) + +func TestHashCodeBlock(t *testing.T) { + t.Parallel() + codeBlock1 := codeBlock{ + content: ` +package main + +func main() { + println("Hello, World") +}`, + lang: "gno", + } + codeBlock2 := codeBlock{ + content: ` +package main + +func main() { + println("Hello, World!") +}`, + lang: "gno", + } + codeBlock3 := codeBlock{ + content: ` +package main + +func main() { + println("Hello, World!") +}`, + lang: "gno", + } + + hashKey1 := hashCodeBlock(codeBlock1) + hashKey2 := hashCodeBlock(codeBlock2) + hashKey3 := hashCodeBlock(codeBlock3) + + if hashKey1 == hashKey2 { + t.Errorf("hash key for code block 1 and 2 are the same: %v", hashKey1) + } + if hashKey2 == hashKey3 { + t.Errorf("hash key for code block 2 and 3 are the same: %v", hashKey2) + } + if hashKey1 == hashKey3 { + t.Errorf("hash key for code block 1 and 3 are the same: %v", hashKey1) + } +} + +func TestExecuteCodeBlock(t *testing.T) { + tests := []struct { + name string + codeBlock codeBlock + expectedResult string + expectError bool + }{ + { + name: "Simple print without expected output", + codeBlock: codeBlock{ + content: ` +package main + +func main() { + println("Hello, World!") +}`, + lang: "gno", + }, + expectedResult: "Hello, World!\n", + }, + { + name: "Print with expected output", + codeBlock: codeBlock{ + content: ` +package main + +func main() { + println("Hello, Gno!") +} +// Output: +// Hello, Gno!`, + lang: "gno", + expectedOutput: "Hello, Gno!", + }, + expectedResult: "Hello, Gno!\n", + }, + { + name: "Print with incorrect expected output", + codeBlock: codeBlock{ + content: ` +package main + +func main() { + println("Hello, Gno!") +} +// Output: +// Hello, World!`, + lang: "gno", + expectedOutput: "Hello, World!", + }, + expectError: true, + }, + { + name: "Code with expected error", + codeBlock: codeBlock{ + content: ` +package main + +func main() { + panic("oops") +} +// Error: +// panic: oops`, + lang: "gno", + expectedError: "panic: oops", + }, + expectError: true, + }, + { + name: "Code with unexpected error", + codeBlock: codeBlock{ + content: ` +package main + +func main() { + panic("unexpected error") +}`, + lang: "gno", + }, + expectError: true, + }, + { + name: "Multiple print statements", + codeBlock: codeBlock{ + content: ` +package main + +func main() { + println("Line 1") + println("Line 2") +} +// Output: +// Line 1 +// Line 2`, + lang: "gno", + expectedOutput: "Line 1\nLine 2", + }, + expectedResult: "Line 1\nLine 2\n", + }, + { + name: "Unsupported language", + codeBlock: codeBlock{ + content: `print("Hello")`, + lang: "python", + }, + expectedResult: "SKIPPED (Unsupported language: python)", + }, + { + name: "Ignored code block", + codeBlock: codeBlock{ + content: ` +package main + +func main() { + panic("This should not execute") +}`, + lang: "gno", + options: ExecutionOptions{ + Ignore: true, + }, + }, + expectedResult: "IGNORED", + }, + { + name: "Should panic code block", + codeBlock: codeBlock{ + content: ` +package main + +func main() { + panic("Expected panic") +}`, + lang: "gno", + options: ExecutionOptions{ + PanicMessage: "Expected panic", + }, + }, + expectedResult: "panicked as expected: Expected panic", + }, + { + name: "Should panic but doesn't", + codeBlock: codeBlock{ + content: ` +package main + +func main() { + println("No panic") +}`, + lang: "gno", + options: ExecutionOptions{ + PanicMessage: "Expected panic", + }, + }, + expectError: true, + }, + { + name: "Should panic with specific message", + codeBlock: codeBlock{ + content: ` +package main + +func main() { + panic("Specific error message") +}`, + lang: "gno", + options: ExecutionOptions{ + PanicMessage: "Specific error message", + }, + }, + expectedResult: "panicked as expected: Specific error message", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := ExecuteCodeBlock(tt.codeBlock, GetStdlibsDir()) + + if tt.expectError { + if err == nil { + t.Errorf("Expected an error, but got none") + } + } else { + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + + if strings.TrimSpace(result) != strings.TrimSpace(tt.expectedResult) { + t.Errorf("Expected result %q, but got %q", tt.expectedResult, result) + } + }) + } +} + +func TestCompareResults(t *testing.T) { + tests := []struct { + name string + actual string + expectedOutput string + expectedError string + wantErr bool + }{ + { + name: "Exact match", + actual: "Hello, World!", + expectedOutput: "Hello, World!", + }, + { + name: "Mismatch", + actual: "Hello, World!", + expectedOutput: "Hello, Gno!", + wantErr: true, + }, + { + name: "Regex match", + actual: "Hello, World!", + expectedOutput: "regex:Hello, \\w+!", + }, + { + name: "Numbers Regex match", + actual: "1234567890", + expectedOutput: "regex:\\d+", + }, + { + name: "Complex Regex match (e-mail format)", + actual: "foobar12456@somemail.com", + expectedOutput: "regex:[a-zA-Z0-9]+@[a-zA-Z0-9]+\\.[a-zA-Z0-9]+", + }, + { + name: "Error match", + actual: "Error: division by zero", + expectedError: "Error: division by zero", + }, + { + name: "Error mismatch", + actual: "Error: division by zero", + expectedError: "Error: null pointer", + wantErr: true, + }, + { + name: "Error regex match", + actual: "Error: division by zero", + expectedError: "regex:Error: .+", + }, + { + name: "Empty expected", + actual: "Hello, World!", + expectedOutput: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := compareResults(tt.actual, tt.expectedOutput, tt.expectedError) + if (err != nil) != tt.wantErr { + t.Errorf("compareResults() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err != nil && tt.wantErr { + if tt.expectedOutput != "" && !strings.Contains(err.Error(), tt.expectedOutput) { + t.Errorf("compareResults() error = %v, should contain %v", err, tt.expectedOutput) + } + if tt.expectedError != "" && !strings.Contains(err.Error(), tt.expectedError) { + t.Errorf("compareResults() error = %v, should contain %v", err, tt.expectedError) + } + } + }) + } +} + +func TestExecuteMatchingCodeBlock(t *testing.T) { + testCases := []struct { + name string + content string + pattern string + expectedResult []string + expectError bool + }{ + { + name: "Single matching block", + content: ` +Some text here +` + "```go" + ` +// @test: test1 +package main + +func main() { + println("Hello, World!") +} +` + "```" + ` +More text +`, + pattern: "test1", + expectedResult: []string{"\n=== test1 ===\n\nHello, World!\n\n"}, + expectError: false, + }, + { + name: "Multiple matching blocks", + content: ` +` + "```go" + ` +// @test: test1 +package main + +func main() { + println("First") +} +` + "```" + ` +` + "```go" + ` +// @test: test2 +package main + +func main() { + println("Second") +} +` + "```" + ` +`, + pattern: "test*", + expectedResult: []string{"\n=== test1 ===\n\nFirst\n\n", "\n=== test2 ===\n\nSecond\n\n"}, + expectError: false, + }, + { + name: "No matching blocks", + content: ` +` + "```go" + ` +// @test: test1 +func main() { + println("Hello") +} +` + "```" + ` +`, + pattern: "nonexistent", + expectedResult: []string{}, + expectError: false, + }, + { + name: "Error in code block", + content: ` +` + "```go" + ` +// @test: error_test +package main + +func main() { + panic("This should cause an error") +} +` + "```" + ` +`, + pattern: "error_test", + expectError: true, + }, + { + name: "expected output is nothing but actual output is something", + content: ` +` + "```go" + ` +// @test: foo +package main + +func main() { + println("This is an unexpected output") +} + +// Output: +` + "```" + ` +`, + pattern: "foo", + expectedResult: []string{"\n=== foo ===\n\nThis is an unexpected output\n\n"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + results, err := ExecuteMatchingCodeBlock(ctx, tc.content, tc.pattern) + + if tc.expectError { + if err == nil { + t.Errorf("Expected an error, but got none") + } + } else { + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if len(results) == 0 && len(tc.expectedResult) == 0 { + // do nothing + } else if !reflect.DeepEqual(results, tc.expectedResult) { + t.Errorf("Expected results %v, but got %v", tc.expectedResult, results) + } + } + + for _, expected := range tc.expectedResult { + found := false + for _, result := range results { + if strings.Contains(result, strings.TrimSpace(expected)) { + found = true + break + } + } + if !found { + t.Errorf("Expected result not found: %s", expected) + } + } + }) + } +} diff --git a/gnovm/pkg/doctest/parser.go b/gnovm/pkg/doctest/parser.go new file mode 100644 index 00000000000..8707dea5e4d --- /dev/null +++ b/gnovm/pkg/doctest/parser.go @@ -0,0 +1,371 @@ +package doctest + +import ( + "bufio" + "bytes" + "fmt" + "go/ast" + "go/parser" + "go/token" + "regexp" + "strings" + + "github.com/yuin/goldmark" + mast "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/text" +) + +// codeBlock represents a block of code extracted from the input text. +type codeBlock struct { + content string // The content of the code block. + start int // The start byte position of the code block in the input text. + end int // The end byte position of the code block in the input text. + lang string // The language type of the code block. + index int // The index of the code block in the sequence of extracted blocks. + expectedOutput string // The expected output of the code block. + expectedError string // The expected error of the code block. + name string // The name of the code block. + options ExecutionOptions +} + +// GetCodeBlocks parses the provided markdown text to extract all embedded code blocks. +// It returns a slice of codeBlock structs, each representing a distinct block of code found in the markdown. +func GetCodeBlocks(body string) []codeBlock { + md := goldmark.New() + reader := text.NewReader([]byte(body)) + doc := md.Parser().Parse(reader) + + var codeBlocks []codeBlock + mast.Walk(doc, func(n mast.Node, entering bool) (mast.WalkStatus, error) { + if entering { + if cb, ok := n.(*mast.FencedCodeBlock); ok { + codeBlock := createCodeBlock(cb, body, len(codeBlocks)) + codeBlock.name = generateCodeBlockName(codeBlock.content, codeBlock.expectedOutput) + codeBlocks = append(codeBlocks, codeBlock) + } + } + return mast.WalkContinue, nil + }) + + return codeBlocks +} + +// createCodeBlock creates a CodeBlock from a code block node. +func createCodeBlock(node *mast.FencedCodeBlock, body string, index int) codeBlock { + var buf bytes.Buffer + lines := node.Lines() + for i := 0; i < lines.Len(); i++ { + line := lines.At(i) + buf.Write([]byte(body[line.Start:line.Stop])) + } + + content := buf.String() + language := string(node.Language([]byte(body))) + if language == "" { + language = "plain" + } + + firstLine := body[lines.At(0).Start:lines.At(0).Stop] + options := parseExecutionOptions(language, []byte(firstLine)) + + start := lines.At(0).Start + end := lines.At(node.Lines().Len() - 1).Stop + + expectedOutput, expectedError, err := parseExpectedResults(content) + if err != nil { + panic(err) + } + + return codeBlock{ + content: content, + start: start, + end: end, + lang: language, + index: index, + expectedOutput: expectedOutput, + expectedError: expectedError, + options: options, + } +} + +// parseExpectedResults scans the code block content for expecting outputs and errors, +// which are typically indicated by special comments in the code. +func parseExpectedResults(content string) (string, string, error) { + outputRegex := regexp.MustCompile(`(?m)^// Output:$([\s\S]*?)(?:^(?://\s*$|// Error:|$))`) + errorRegex := regexp.MustCompile(`(?m)^// Error:$([\s\S]*?)(?:^(?://\s*$|// Output:|$))`) + + var outputs, errors []string + + outputMatches := outputRegex.FindAllStringSubmatch(content, -1) + for _, match := range outputMatches { + if len(match) > 1 { + cleaned, err := cleanSection(match[1]) + if err != nil { + return "", "", err + } + if cleaned != "" { + outputs = append(outputs, cleaned) + } + } + } + + errorMatches := errorRegex.FindAllStringSubmatch(content, -1) + for _, match := range errorMatches { + if len(match) > 1 { + cleaned, err := cleanSection(match[1]) + if err != nil { + return "", "", err + } + if cleaned != "" { + errors = append(errors, cleaned) + } + } + } + + expectedOutput := strings.Join(outputs, "\n") + expectedError := strings.Join(errors, "\n") + + return expectedOutput, expectedError, nil +} + +func cleanSection(section string) (string, error) { + scanner := bufio.NewScanner(strings.NewReader(section)) + var cleanedLines []string + + for scanner.Scan() { + line := strings.TrimSpace(strings.TrimPrefix(scanner.Text(), "//")) + line = strings.TrimPrefix(line, " ") + if line != "" { + cleanedLines = append(cleanedLines, line) + } + } + + if err := scanner.Err(); err != nil { + return "", fmt.Errorf("failed to clean section: %w", err) + } + + return strings.Join(cleanedLines, "\n"), nil +} + +//////////////////// Auto-Name Generator //////////////////// + +// generateCodeBlockName generates a name for a given code block based on its content. +// It first checks for a custom name specified with `// @test:` comment. +// If not found, it analyzes the code structure to create meaningful name. +// The name is constructed based on the code's prefix (Test, Print or Calc), +// imported packages, main identifier, and expected output. +func generateCodeBlockName(content string, expectedOutput string) string { + scanner := bufio.NewScanner(strings.NewReader(content)) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(strings.TrimSpace(line), "// @test:") { + return strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), "// @test:")) + } + } + + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "", content, parser.ParseComments) + if err != nil { + return generateFallbackName(content) + } + + prefix := determinePrefix(f) + imports := extractImports(f) + mainIdentifier := extractMainIdentifier(f) + + name := constructName(prefix, imports, expectedOutput, mainIdentifier) + + return name +} + +// determinePrefix analyzes the AST of a file and determines an appropriate prefix +// for the code block name. +// It returns "Test" for test functions, "Print" for functions containing print statements, +// "Calc" for function containing calculations, or an empty string if no specific prefix is determined. +func determinePrefix(f *ast.File) string { + // determine the prefix by using heuristic + for _, decl := range f.Decls { + if fn, ok := decl.(*ast.FuncDecl); ok { + if strings.HasPrefix(fn.Name.Name, "Test") { + return "Test" + } + if containsPrintStmt(fn) { + return "Print" + } + if containsCalculation(fn) { + return "Calc" + } + } + } + return "" +} + +// containsPrintStmt checks if the given function declaration contains +// any print or println statements. +func containsPrintStmt(fn *ast.FuncDecl) bool { + var hasPrintStmt bool + ast.Inspect(fn, func(n ast.Node) bool { + if call, ok := n.(*ast.CallExpr); ok { + if ident, ok := call.Fun.(*ast.Ident); ok { + if ident.Name == "println" || ident.Name == "print" { + hasPrintStmt = true + return false + } + } + } + return true + }) + return hasPrintStmt +} + +// containsCalculation checks if the given function declaration contains +// any binary or unary expressions, which are indicative of calculations. +func containsCalculation(fn *ast.FuncDecl) bool { + var hasCalcExpr bool + ast.Inspect(fn, func(n ast.Node) bool { + switch n.(type) { + case *ast.BinaryExpr, *ast.UnaryExpr: + hasCalcExpr = true + return false + } + return true + }) + return hasCalcExpr +} + +// extractImports extracts the names of imported packages from the AST +// of a Go file. It returns a slice of strings representing the imported +// package names or the last part of the import path if no alias is used. +func extractImports(f *ast.File) []string { + imports := make([]string, 0) + for _, imp := range f.Imports { + if imp.Name != nil { + imports = append(imports, imp.Name.Name) + continue + } + path := strings.Trim(imp.Path.Value, `"`) + parts := strings.Split(path, "/") + imports = append(imports, parts[len(parts)-1]) + } + return imports +} + +// extractMainIdentifier attempts to find the main identifier in the Go file. +// It returns the name of the first function or the first declared variable. +// If no suitable identifier is found, it returns an empty string. +func extractMainIdentifier(f *ast.File) string { + for _, decl := range f.Decls { + switch d := decl.(type) { + case *ast.FuncDecl: + return d.Name.Name + case *ast.GenDecl: + for _, spec := range d.Specs { + if vs, ok := spec.(*ast.ValueSpec); ok { + if len(vs.Names) > 0 { + return vs.Names[0].Name + } + } + } + } + } + return "" +} + +// constructName builds a name for the code block using the provided components. +// The resulting name is truncated if it exceeds 50 characters. +func constructName( + prefix string, + imports []string, + expectedOutput string, + mainIdentifier string, +) string { + var parts []string + if prefix != "" { + parts = append(parts, prefix) + } + if mainIdentifier != "" { + parts = append(parts, mainIdentifier) + } + if expectedOutput != "" { + // use first line of expected output, limit the length + outputPart := strings.Split(expectedOutput, "\n")[0] + if len(outputPart) > 20 { + outputPart = outputPart[:20] + "..." + } + parts = append(parts, outputPart) + } + + // Add imports last, limiting to a certain number of characters + if len(imports) > 0 { + importString := strings.Join(imports, "_") + if len(importString) > 30 { + importString = importString[:30] + "..." + } + parts = append(parts, importString) + } + + name := strings.Join(parts, "_") + if len(name) > 50 { + name = name[:50] + "..." + } + + return name +} + +// generateFallbackName generates a default name for a code block when no other name could be determined. +// It uses the first significant line of the code that is not a comment or package declaration. +func generateFallbackName(content string) string { + scanner := bufio.NewScanner(strings.NewReader(content)) + for scanner.Scan() { + trimmed := strings.TrimSpace(scanner.Text()) + if trimmed != "" && !strings.HasPrefix(trimmed, "//") && trimmed != "package main" { + if len(trimmed) > 20 { + return trimmed[:20] + "..." + } + return trimmed + } + } + return "unnamed_block" +} + +//////////////////// Execution Options //////////////////// + +type ExecutionOptions struct { + Ignore bool + PanicMessage string + // TODO: add more options +} + +func parseExecutionOptions(language string, firstLine []byte) ExecutionOptions { + var options ExecutionOptions + + parts := strings.Split(language, ",") + for _, option := range parts[1:] { // skip the first part which is the language + switch strings.TrimSpace(option) { + case "ignore": + options.Ignore = true + case "should_panic": + // specific panic message will be parsed later + } + } + + // parser options from the first line of the code block + if bytes.HasPrefix(firstLine, []byte("//")) { + // parse execution options from the first line of the code block + // e.g. // @should_panic="some panic message here" + // |-option name-||-----option value-----| + re := regexp.MustCompile(`@(\w+)(?:="([^"]*)")?`) + matches := re.FindAllSubmatch(firstLine, -1) + for _, match := range matches { + switch string(match[1]) { + case "should_panic": + if match[2] != nil { + options.PanicMessage = string(match[2]) + } + // TODO: add more options + } + } + } + + return options +} diff --git a/gnovm/pkg/doctest/parser_test.go b/gnovm/pkg/doctest/parser_test.go new file mode 100644 index 00000000000..95ea1a86787 --- /dev/null +++ b/gnovm/pkg/doctest/parser_test.go @@ -0,0 +1,462 @@ +package doctest + +import ( + "reflect" + "strings" + "testing" +) + +func TestGetCodeBlocks(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input string + expected []codeBlock + }{ + { + name: "Single code block with backticks", + input: "```go\nfmt.Println(\"Hello, World!\")\n```", + expected: []codeBlock{ + { + content: "fmt.Println(\"Hello, World!\")", + start: 6, + end: 35, + lang: "go", + index: 0, + }, + }, + }, + { + name: "Single code block with additional backticks", + input: "```go\nfmt.Println(\"Hello, World!\")\n``````", + expected: []codeBlock{ + { + content: "fmt.Println(\"Hello, World!\")", + start: 6, + end: 35, + lang: "go", + index: 0, + }, + }, + }, + { + name: "Single code block with tildes", + input: "## Example\nprint hello world in go.\n~~~go\nfmt.Println(\"Hello, World!\")\n~~~", + expected: []codeBlock{ + { + content: "fmt.Println(\"Hello, World!\")", + start: 42, + end: 71, + lang: "go", + index: 0, + }, + }, + }, + { + name: "Multiple code blocks", + input: "Here is some text.\n```python\ndef hello():\n print(\"Hello, World!\")\n```\nSome more text.\n```javascript\nconsole.log(\"Hello, World!\");\n```", + expected: []codeBlock{ + { + content: "def hello():\n print(\"Hello, World!\")", + start: 29, + end: 69, + lang: "python", + index: 0, + }, + { + content: "console.log(\"Hello, World!\");", + start: 103, + end: 133, + lang: "javascript", + index: 1, + }, + }, + }, + { + name: "Code block with no language specifier", + input: "```\nfmt.Println(\"Hello, World!\")\n```", + expected: []codeBlock{ + { + content: "fmt.Println(\"Hello, World!\")", + start: 4, + end: 33, + lang: "plain", + index: 0, + }, + }, + }, + { + name: "No code blocks", + input: "Just some text without any code blocks.", + expected: nil, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := GetCodeBlocks(tt.input) + if len(result) != len(tt.expected) { + t.Errorf("Failed %s: expected %d code blocks, got %d", tt.name, len(tt.expected), len(result)) + } + + for i, res := range result { + if normalize(res.content) != normalize(tt.expected[i].content) { + t.Errorf("Failed %s: expected content %s, got %s", tt.name, tt.expected[i].content, res.content) + } + + if res.start != tt.expected[i].start { + t.Errorf("Failed %s: expected start %d, got %d", tt.name, tt.expected[i].start, res.start) + } + + if res.end != tt.expected[i].end { + t.Errorf("Failed %s: expected end %d, got %d", tt.name, tt.expected[i].end, res.end) + } + + if res.lang != tt.expected[i].lang { + t.Errorf("Failed %s: expected type %s, got %s", tt.name, tt.expected[i].lang, res.lang) + } + + if res.index != tt.expected[i].index { + t.Errorf("Failed %s: expected index %d, got %d", tt.name, tt.expected[i].index, res.index) + } + } + }) + } +} + +func TestParseExpectedResults(t *testing.T) { + tests := []struct { + name string + content string + wantOutput string + wantError string + wantParseError bool + }{ + { + name: "Basic output", + content: ` +// Some code +fmt.Println("Hello, World!") +// Output: +// Hello, World! +`, + wantOutput: "Hello, World!", + wantError: "", + }, + { + name: "Basic error", + content: ` +// Some code that causes an error +panic("oops") +// Error: +// panic: oops +`, + wantOutput: "", + wantError: "panic: oops", + }, + { + name: "Output and error", + content: ` +// Some code with both output and error +fmt.Println("Start") +panic("oops") +// Output: +// Start +// Error: +// panic: oops +`, + wantOutput: "Start", + wantError: "panic: oops", + }, + { + name: "Multiple output sections", + content: ` +// First output +fmt.Println("Hello") +// Output: +// Hello +// World +`, + wantOutput: "Hello\nWorld", + wantError: "", + }, + { + name: "Preserve indentation", + content: ` +// Indented output +fmt.Println(" Indented") +// Output: +// Indented +`, + wantOutput: "Indented", + wantError: "", + }, + { + name: "Output with // in content", + content: ` +// Output with // +fmt.Println("// Comment") +// Output: +// // Comment +`, + wantOutput: "// Comment", + wantError: "", + }, + { + name: "Empty content", + content: ` +// Just some comments +// No output or error +`, + wantOutput: "", + wantError: "", + }, + { + name: "simple code", + content: ` +package main + +func main() { + println("Actual output") +} +// Output: +// Actual output +`, + wantOutput: "Actual output", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotOutput, gotError, err := parseExpectedResults(tt.content) + if (err != nil) != tt.wantParseError { + t.Errorf("parseExpectedResults() error = %v, wantParseError %v", err, tt.wantParseError) + return + } + if gotOutput != tt.wantOutput { + t.Errorf("parseExpectedResults() gotOutput = %v, want %v", gotOutput, tt.wantOutput) + } + if gotError != tt.wantError { + t.Errorf("parseExpectedResults() gotError = %v, want %v", gotError, tt.wantError) + } + }) + } +} + +func TestGenerateCodeBlockName(t *testing.T) { + tests := []struct { + name string + content string + output string + expectedGenerateName string + }{ + { + name: "Simple print function", + content: ` +package main + +func main() { + println("Hello, World!") +} +// Output: +// Hello, World! +`, + output: "Hello, World!", + expectedGenerateName: "Print_main_Hello, World!", + }, + { + name: "Explicitly named code block", + content: ` +// @test: specified +package main + +func main() { + println("specified") +}`, + output: "specified", + expectedGenerateName: "specified", + }, + { + name: "Simple calculation", + content: ` +package main + +import "math" + +func calculateArea(radius float64) float64 { + return math.Pi * radius * radius +} + +func main() { + println(calculateArea(5)) +} +// Output: +// 78.53981633974483 +`, + output: "78.53981633974483", + expectedGenerateName: "Calc_calculateArea_78.53981633974483_math", + }, + { + name: "Test function", + content: ` +package main + +import "testing" + +func TestSquareRoot(t *testing.T) { + got := math.Sqrt(4) + if got != 2 { + t.Errorf("Sqrt(4) = %f; want 2", got) + } +} +`, + expectedGenerateName: "Test_TestSquareRoot_testing", + }, + { + name: "Multiple imports", + content: ` +package main + +import ( + "math" + "strings" +) + +func main() { + println(math.Pi) + println(strings.ToUpper("hello")) +} +// Output: +// 3.141592653589793 +// HELLO +`, + output: "3.141592653589793\nHELLO", + expectedGenerateName: "Print_main_3.141592653589793_math_strings", + }, + { + name: "No function", + content: ` +package main + +var x = 5 +`, + expectedGenerateName: "x", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := generateCodeBlockName(tt.content, tt.output) + if result != tt.expectedGenerateName { + t.Errorf("generateCodeBlockName() = %v, want %v", result, tt.expectedGenerateName) + } + }) + } +} + +func TestParseExecutionOptions(t *testing.T) { + tests := []struct { + name string + language string + firstLine string + want ExecutionOptions + }{ + { + name: "No options", + language: "go", + firstLine: "package main", + want: ExecutionOptions{}, + }, + { + name: "Ignore option in language tag", + language: "go,ignore", + firstLine: "package main", + want: ExecutionOptions{Ignore: true}, + }, + { + name: "Should panic option in language tag", + language: "go,should_panic", + firstLine: "package main", + want: ExecutionOptions{PanicMessage: ""}, + }, + { + name: "Should panic with message in comment", + language: "go,should_panic", + firstLine: "// @should_panic=\"division by zero\"", + want: ExecutionOptions{PanicMessage: "division by zero"}, + }, + { + name: "Multiple options", + language: "go,ignore,should_panic", + firstLine: "// @should_panic=\"runtime error\"", + want: ExecutionOptions{Ignore: true, PanicMessage: "runtime error"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := parseExecutionOptions(tt.language, []byte(tt.firstLine)) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseExecutionOptions() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetCodeBlocksWithOptions(t *testing.T) { + input := ` +Some text here + +` + "```go,ignore" + ` +// This block should be ignored +func main() { + panic("This should not execute") +} +` + "```" + ` + +Another paragraph + +` + "```go,should_panic" + ` +// @should_panic="runtime error: index out of range" +func main() { + arr := []int{1, 2, 3} + fmt.Println(arr[5]) +} +` + "```" + ` + +` + "```go" + ` +// Normal execution +func main() { + fmt.Println("Hello, World!") +} +` + "```" + ` +` + + blocks := GetCodeBlocks(input) + + if len(blocks) != 3 { + t.Fatalf("Expected 3 code blocks, got %d", len(blocks)) + } + + // Check the first block (ignore) + if !blocks[0].options.Ignore { + t.Errorf("Expected first block to be ignored") + } + + // Check the second block (should_panic) + if blocks[1].options.PanicMessage != "runtime error: index out of range" { + t.Errorf("Expected second block to have ShouldPanic option set to 'runtime error: index out of range', got '%s'", blocks[1].options.PanicMessage) + } + + // Check the third block (normal execution) + if blocks[2].options.Ignore || blocks[2].options.PanicMessage != "" { + t.Errorf("Expected third block to have no special options") + } +} + +// ignore whitespace in the source code +func normalize(s string) string { + return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(s, "\n", ""), "\r", ""), "\t", ""), " ", "") +} diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index 850da3d3c0f..f7e2b3d4d05 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -217,7 +217,7 @@ func (m *Machine) SetActivePackage(pv *PackageValue) { // This is a temporary measure until we optimize/make-lazy. // // NOTE: package paths not beginning with gno.land will be allowed to override, -// to support cases of stdlibs processed through [RunMemPackagesWithOverrides]. +// to support cases of stdlibs processed through [RunMemPackageWithOverrides]. func (m *Machine) PreprocessAllFilesAndSaveBlockNodes() { ch := m.Store.IterMemPackage() for memPkg := range ch { diff --git a/gnovm/pkg/gnolang/nodes_test.go b/gnovm/pkg/gnolang/nodes_test.go index 2c3a03d8c09..c602310820b 100644 --- a/gnovm/pkg/gnolang/nodes_test.go +++ b/gnovm/pkg/gnolang/nodes_test.go @@ -1,10 +1,13 @@ -package gnolang_test +package gnolang import ( "math" + "os" + "path/filepath" "testing" - "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestStaticBlock_Define2_MaxNames(t *testing.T) { @@ -26,12 +29,12 @@ func TestStaticBlock_Define2_MaxNames(t *testing.T) { t.Errorf("expected panic when exceeding maximum number of names") }() - staticBlock := new(gnolang.StaticBlock) + staticBlock := new(StaticBlock) staticBlock.NumNames = math.MaxUint16 - 1 - staticBlock.Names = make([]gnolang.Name, staticBlock.NumNames) + staticBlock.Names = make([]Name, staticBlock.NumNames) // Adding one more is okay. - staticBlock.Define2(false, gnolang.Name("a"), gnolang.BoolType, gnolang.TypedValue{T: gnolang.BoolType}) + staticBlock.Define2(false, Name("a"), BoolType, TypedValue{T: BoolType}) if staticBlock.NumNames != math.MaxUint16 { t.Errorf("expected NumNames to be %d, got %d", math.MaxUint16, staticBlock.NumNames) } @@ -40,5 +43,44 @@ func TestStaticBlock_Define2_MaxNames(t *testing.T) { } // This one should panic because the maximum number of names has been reached. - staticBlock.Define2(false, gnolang.Name("a"), gnolang.BoolType, gnolang.TypedValue{T: gnolang.BoolType}) + staticBlock.Define2(false, Name("a"), BoolType, TypedValue{T: BoolType}) +} + +func TestReadMemPackage(t *testing.T) { + tempDir, err := os.MkdirTemp("", "testpkg") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + // Create valid files + validFiles := []string{"file1.gno", "README.md", "LICENSE", "gno.mod"} + for _, f := range validFiles { + err := os.WriteFile(filepath.Join(tempDir, f), []byte(` + package main + + import ( + "gno.land/p/demo/ufmt" + ) + + func main() { + ufmt.Printfln("Hello, World!") + }`), 0o644) + require.NoError(t, err) + } + + // Create invalid files + invalidFiles := []string{".hiddenfile", "unsupported.txt"} + for _, f := range invalidFiles { + err := os.WriteFile(filepath.Join(tempDir, f), []byte("content"), 0o644) + require.NoError(t, err) + } + + // Test Case 1: Valid Package Directory + memPkg := ReadMemPackage(tempDir, "testpkg") + require.NotNil(t, memPkg) + assert.Len(t, memPkg.Files, len(validFiles), "MemPackage should contain only valid files") + + // Test Case 2: Non-existent Directory + assert.Panics(t, func() { + ReadMemPackage("/non/existent/dir", "testpkg") + }, "Expected panic for non-existent directory") } diff --git a/go.mod b/go.mod index d0845d73641..49aca4a0aec 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( github.com/rs/xid v1.5.0 github.com/stretchr/testify v1.9.0 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 + github.com/yuin/goldmark v1.7.3 go.etcd.io/bbolt v1.3.10 go.opentelemetry.io/otel v1.28.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 diff --git a/go.sum b/go.sum index 5cb6be26da2..67fcf9313eb 100644 --- a/go.sum +++ b/go.sum @@ -150,6 +150,8 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.3 h1:fdk0a/y60GsS4NbEd13GSIP+d8OjtTkmluY32Dy1Z/A= +github.com/yuin/goldmark v1.7.3/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw=