lotus/tvx/builders/generator.go

187 lines
4.7 KiB
Go

package builders
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"os"
"path/filepath"
"regexp"
"sync"
"github.com/filecoin-project/oni/tvx/schema"
)
// Generator is a batch generator and organizer of test vectors.
//
// Test vector scripts are simple programs (main function). Test vector scripts
// can delegate to the Generator to handle the execution, reporting and capture
// of emitted test vectors into files.
//
// Generator supports the following CLI flags:
//
// -o <directory>
// directory where test vector JSON files will be saved; if omitted,
// vectors will be written to stdout.
//
// -f <regex>
// regex filter to select a subset of vectors to execute; matched against
// the vector's ID.
//
// Scripts can bundle test vectors into "groups". The generator will execute
// each group in parallel, and will write each vector in a file:
// <output_dir>/<group>--<vector_id>.json
type Generator struct {
OutputPath string
Filter *regexp.Regexp
wg sync.WaitGroup
}
// genData is the generation data to stamp into vectors.
// TODO in the future this should contain the commit of this tool and
// the builder api.
var genData = schema.GenerationData{
Source: "script",
Version: "v0",
}
type MessageVectorGenItem struct {
Metadata *schema.Metadata
Func func(*Builder)
}
func NewGenerator() *Generator {
// Consume CLI parameters.
var (
outputDir = flag.String("o", "", "directory where test vector JSON files will be saved; if omitted, vectors will be written to stdout")
filter = flag.String("f", "", "regex filter to select a subset of vectors to execute; matched against the vector's ID")
)
flag.Parse()
ret := new(Generator)
// If output directory is provided, we ensure it exists, or create it.
// Else, we'll output to stdout.
if dir := *outputDir; dir != "" {
err := ensureDirectory(dir)
if err != nil {
log.Fatal(err)
}
ret.OutputPath = dir
}
// If a filter has been provided, compile it into a regex.
if *filter != "" {
exp, err := regexp.Compile(*filter)
if err != nil {
log.Fatalf("supplied regex %s is invalid: %s", *filter, err)
}
ret.Filter = exp
}
return ret
}
func (g *Generator) Wait() {
g.wg.Wait()
}
func (g *Generator) MessageVectorGroup(group string, vectors ...*MessageVectorGenItem) {
g.wg.Add(1)
go func() {
defer g.wg.Done()
var wg sync.WaitGroup
for _, item := range vectors {
if id := item.Metadata.ID; g.Filter != nil && !g.Filter.MatchString(id) {
log.Printf("skipping %s", id)
continue
}
var w io.Writer
if g.OutputPath == "" {
w = os.Stdout
} else {
file := filepath.Join(g.OutputPath, fmt.Sprintf("%s--%s.json", group, item.Metadata.ID))
out, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
log.Printf("failed to write to file %s: %s", file, err)
return
}
w = out
}
wg.Add(1)
go func(item *MessageVectorGenItem) {
g.generateOne(w, item, w != os.Stdout)
wg.Done()
}(item)
}
wg.Wait()
}()
}
func (g *Generator) generateOne(w io.Writer, b *MessageVectorGenItem, indent bool) {
log.Printf("generating test vector: %s", b.Metadata.ID)
// stamp with our generation data.
b.Metadata.Gen = genData
vector := MessageVector(b.Metadata)
// TODO: currently if an assertion fails, we call os.Exit(1), which
// aborts all ongoing vector generations. The Asserter should
// call runtime.Goexit() instead so only that goroutine is
// cancelled. The assertion error must bubble up somehow.
b.Func(vector)
buf := new(bytes.Buffer)
vector.Finish(buf)
final := buf
if indent {
// reparse and reindent.
final = new(bytes.Buffer)
if err := json.Indent(final, buf.Bytes(), "", "\t"); err != nil {
log.Printf("failed to indent json: %s", err)
}
}
n, err := w.Write(final.Bytes())
if err != nil {
log.Printf("failed to write to output: %s", err)
return
}
log.Printf("generated test vector: %s (size: %d bytes)", b.Metadata.ID, n)
}
// ensureDirectory checks if the provided path is a directory. If yes, it
// returns nil. If the path doesn't exist, it creates the directory and
// returns nil. If the path is not a directory, or another error occurs, an
// error is returned.
func ensureDirectory(path string) error {
switch stat, err := os.Stat(path); {
case os.IsNotExist(err):
// create directory.
log.Printf("creating directory %s", path)
err := os.MkdirAll(path, 0700)
if err != nil {
return fmt.Errorf("failed to create directory %s: %s", path, err)
}
case err == nil && !stat.IsDir():
return fmt.Errorf("path %s exists, but it's not a directory", path)
case err != nil:
return fmt.Errorf("failed to stat directory %s: %w", path, err)
}
return nil
}