-
Notifications
You must be signed in to change notification settings - Fork 385
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(gnovm): Executable Markdown #2357
base: master
Are you sure you want to change the base?
Changes from all commits
1e598f6
168259d
edb648e
f60cb1c
a577afc
b29530c
6430862
b7ef8ad
b649727
e53482b
f0df3b3
599bd6f
da567e3
2d363bc
83d7ca0
f31a102
4f05a8e
23052b9
fc94da3
3844365
c8b0aef
5a8a2d8
86bdb99
fbe7ddf
e001a33
e648334
82e02f0
273abb5
1ee5486
4775fd3
855bbc5
cbc060f
ad329eb
bf480c7
1f7f238
1bb104a
6bac3ad
4978329
1577034
2d64669
317fd41
ae25209
45265f2
b7a3d9d
1776969
3142978
d15d3f1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <markdown_file_path> [-run <pattern>] [-timeout <duration>]", | ||
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() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can use error groups, and easily exit when the first routine encounters a problem |
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think about formatting it to seconds? |
||
} | ||
|
||
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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <markdown_file_path> -run <code_block_name | ""> | ||
|
||
- `<markdown_file_path>`: Path to the markdown file containing Gno code blocks | ||
- `<code_block_name>`: 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This this a great idea, I have a few questions: Do we intent to run both go code and gno code in the doctest? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a really good question. Currrently, when rendering markdown, gno doesn't have universal syntax highlighting applied yet (except the custom syntax rule). So, even in official documents like "Effective gno" are using Considering the purpose of markdown is more for documentation rather than testing, this was an inevitable purpose. So I made it recognize both languages. However, it doesn't clearly distinguish whether it's completely To summarize in the order of your questions:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The main difference between Gno and Go is that Gno has a few native methods unique to blockchain usage. For example: std.AssertOriginCall Basically, a program can either be Go code or Gno code. We cannot mix them in one source file. There are a few options I can think of
|
||
// @test: print hello world | ||
package main | ||
|
||
func main() { | ||
println("Hello, World!") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the println does not print correct results compared with file tests.
doctest // Output: file test result is correct. ints is a type defined in main package. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems this is happening because the code is executed through the func ExecuteCodeBlock(c codeBlock, stdlibDir string) (string, error) {
// ...
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)
// ...
} I used this method to handle stdlibs imports. But honestly, I don't know how to maintain this functionality while also making it output There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about replacing it with the proper type when displaying the output in the Markdown? |
||
} | ||
|
||
// 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) | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is nothing formatted here