add mdtest command to generate and run tests from a markdown file
This commit is contained in:
parent
47ff44303f
commit
2ffe24630b
|
@ -0,0 +1,11 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alexflint/go-arg/v2"
|
||||||
|
{{if contains .Code "fmt."}}"fmt"{{end}}
|
||||||
|
{{if contains .Code "strings."}}"strings"{{end}}
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
{{.Code}}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alexflint/go-arg/v2"
|
||||||
|
{{if contains .Code "fmt."}}"fmt"{{end}}
|
||||||
|
{{if contains .Code "strings."}}"strings"{{end}}
|
||||||
|
)
|
||||||
|
|
||||||
|
{{.Code}}
|
|
@ -0,0 +1,179 @@
|
||||||
|
// mdtest executes code blocks in markdown and checks that they run as expected
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alexflint/go-arg/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// var pattern = "```go(.*)```\\s*```\\s*\\$(.*)\\n(.*)```"
|
||||||
|
var pattern = "(?s)```go([^`]*?)```\\s*```([^`]*?)```" //go(.*)```\\s*```\\s*\\$(.*)\\n(.*)```"
|
||||||
|
|
||||||
|
var re = regexp.MustCompile(pattern)
|
||||||
|
|
||||||
|
var funcs = map[string]any{
|
||||||
|
"contains": strings.Contains,
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed example1.go.tpl
|
||||||
|
var templateSource1 string
|
||||||
|
|
||||||
|
//go:embed example2.go.tpl
|
||||||
|
var templateSource2 string
|
||||||
|
|
||||||
|
var t1 = template.Must(template.New("example1.go").Funcs(funcs).Parse(templateSource1))
|
||||||
|
var t2 = template.Must(template.New("example2.go").Funcs(funcs).Parse(templateSource2))
|
||||||
|
|
||||||
|
type payload struct {
|
||||||
|
Code string
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCode(ctx context.Context, code []byte, cmd string) ([]byte, error) {
|
||||||
|
dir, err := os.MkdirTemp("", "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating temp dir to build and run code: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(dir)
|
||||||
|
fmt.Println(strings.Repeat("-", 80))
|
||||||
|
|
||||||
|
srcpath := filepath.Join(dir, "src.go")
|
||||||
|
binpath := filepath.Join(dir, "example")
|
||||||
|
|
||||||
|
// If the code contains a main function then use t2, otherwise use t1
|
||||||
|
t := t1
|
||||||
|
if strings.Contains(string(code), "func main") {
|
||||||
|
t = t2
|
||||||
|
}
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
err = t.Execute(&b, payload{Code: string(code)})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error executing template for source file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(b.String())
|
||||||
|
fmt.Println(strings.Repeat("-", 80))
|
||||||
|
|
||||||
|
err = os.WriteFile(srcpath, b.Bytes(), os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error writing temporary source file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
compiler, err := exec.LookPath("go")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not find path to go compiler: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildCmd := exec.CommandContext(ctx, compiler, "build", "-o", binpath, srcpath)
|
||||||
|
out, err := buildCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error building source: %w. Compiler said:\n%s", err, string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace "./example" with full path to compiled program
|
||||||
|
var env, args []string
|
||||||
|
var found bool
|
||||||
|
for _, part := range strings.Split(cmd, " ") {
|
||||||
|
if found {
|
||||||
|
args = append(args, part)
|
||||||
|
} else if part == "./example" {
|
||||||
|
found = true
|
||||||
|
} else {
|
||||||
|
env = append(env, part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runCmd := exec.CommandContext(ctx, binpath, args...)
|
||||||
|
runCmd.Env = env
|
||||||
|
output, err := runCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error runing example: %w. Program said:\n%s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up the temp dir
|
||||||
|
if err := os.RemoveAll(dir); err != nil {
|
||||||
|
return nil, fmt.Errorf("error deleting temp dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
Input string `arg:"positional,required"`
|
||||||
|
}
|
||||||
|
arg.MustParse(&args)
|
||||||
|
|
||||||
|
buf, err := os.ReadFile(args.Input)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(strings.Repeat("=", 80))
|
||||||
|
|
||||||
|
matches := re.FindAllSubmatchIndex(buf, -1)
|
||||||
|
for k, match := range matches {
|
||||||
|
codebegin, codeend := match[2], match[3]
|
||||||
|
code := buf[codebegin:codeend]
|
||||||
|
|
||||||
|
shellbegin, shellend := match[4], match[5]
|
||||||
|
shell := buf[shellbegin:shellend]
|
||||||
|
|
||||||
|
lines := strings.Split(string(shell), "\n")
|
||||||
|
for i := 0; i < len(lines); i++ {
|
||||||
|
if strings.HasPrefix(lines[i], "$") && strings.Contains(lines[i], "./example") {
|
||||||
|
cmd := strings.TrimSpace(strings.TrimPrefix(lines[i], "$"))
|
||||||
|
|
||||||
|
var output []string
|
||||||
|
i++
|
||||||
|
for i < len(lines) && !strings.HasPrefix(lines[i], "$") {
|
||||||
|
output = append(output, lines[i])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := strings.TrimSpace(strings.Join(output, "\n"))
|
||||||
|
|
||||||
|
fmt.Println(string(code))
|
||||||
|
fmt.Println(strings.Repeat("-", 80))
|
||||||
|
fmt.Println(string(cmd))
|
||||||
|
fmt.Println(strings.Repeat("-", 80))
|
||||||
|
fmt.Println(string(expected))
|
||||||
|
fmt.Println(strings.Repeat("-", 80))
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
actual, err := runCode(ctx, code, cmd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error running example %d: %w\nCode was:\n%s", k, err, string(code))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(string(actual))
|
||||||
|
fmt.Println(strings.Repeat("=", 80))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("found %d matches\n", len(matches))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := Main(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue