Move source files to root dir
This commit is contained in:
26
engine/default.go
Normal file
26
engine/default.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"git.defalsify.org/festive/cache"
|
||||
"git.defalsify.org/festive/resource"
|
||||
"git.defalsify.org/festive/state"
|
||||
)
|
||||
|
||||
// NewDefaultEngine is a convenience function to instantiate a filesystem-backed engine with no output constraints.
|
||||
func NewDefaultEngine(dir string) Engine {
|
||||
st := state.NewState(0)
|
||||
rs := resource.NewFsResource(dir)
|
||||
ca := cache.NewCache()
|
||||
return NewEngine(Config{}, &st, &rs, ca)
|
||||
}
|
||||
|
||||
// NewSizedEngine is a convenience function to instantiate a filesystem-backed engine with a specified output constraint.
|
||||
func NewSizedEngine(dir string, size uint32) Engine {
|
||||
st := state.NewState(0)
|
||||
rs := resource.NewFsResource(dir)
|
||||
ca := cache.NewCache()
|
||||
cfg := Config{
|
||||
OutputSize: size,
|
||||
}
|
||||
return NewEngine(cfg, &st, &rs, ca)
|
||||
}
|
||||
127
engine/engine.go
Normal file
127
engine/engine.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
|
||||
"git.defalsify.org/festive/cache"
|
||||
"git.defalsify.org/festive/render"
|
||||
"git.defalsify.org/festive/resource"
|
||||
"git.defalsify.org/festive/state"
|
||||
"git.defalsify.org/festive/vm"
|
||||
)
|
||||
|
||||
// Config globally defines behavior of all components driven by the engine.
|
||||
type Config struct {
|
||||
OutputSize uint32 // Maximum size of output from a single rendered page
|
||||
}
|
||||
|
||||
// Engine is an execution engine that handles top-level errors when running client inputs against code in the bytecode buffer.
|
||||
type Engine struct {
|
||||
st *state.State
|
||||
rs resource.Resource
|
||||
ca cache.Memory
|
||||
vm *vm.Vm
|
||||
}
|
||||
|
||||
// NewEngine creates a new Engine
|
||||
func NewEngine(cfg Config, st *state.State, rs resource.Resource, ca cache.Memory) Engine {
|
||||
var szr *render.Sizer
|
||||
if cfg.OutputSize > 0 {
|
||||
szr = render.NewSizer(cfg.OutputSize)
|
||||
}
|
||||
engine := Engine{
|
||||
st: st,
|
||||
rs: rs,
|
||||
ca: ca,
|
||||
vm: vm.NewVm(st, rs, ca, szr),
|
||||
}
|
||||
return engine
|
||||
}
|
||||
|
||||
// Init must be explicitly called before using the Engine instance.
|
||||
//
|
||||
// It loads and executes code for the start node.
|
||||
func(en *Engine) Init(sym string, ctx context.Context) error {
|
||||
err := en.st.SetInput([]byte{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b := vm.NewLine(nil, vm.MOVE, []string{sym}, nil, nil)
|
||||
b, err = en.vm.Run(b, ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
en.st.SetCode(b)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Exec processes user input against the current state of the virtual machine environment.
|
||||
//
|
||||
// If successfully executed, output of the last execution is available using the WriteResult call.
|
||||
//
|
||||
// A bool return valus of false indicates that execution should be terminated. Calling Exec again has undefined effects.
|
||||
//
|
||||
// Fails if:
|
||||
// - input is formally invalid (too long etc)
|
||||
// - no current bytecode is available
|
||||
// - input processing against bytcode failed
|
||||
func (en *Engine) Exec(input []byte, ctx context.Context) (bool, error) {
|
||||
err := vm.ValidInput(input)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
err = en.st.SetInput(input)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
log.Printf("new execution with input '%s' (0x%x)", input, input)
|
||||
code, err := en.st.GetCode()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(code) == 0 {
|
||||
return false, fmt.Errorf("no code to execute")
|
||||
}
|
||||
code, err = en.vm.Run(code, ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
v, err := en.st.MatchFlag(state.FLAG_TERMINATE, false)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if v {
|
||||
if len(code) > 0 {
|
||||
log.Printf("terminated with code remaining: %x", code)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
en.st.SetCode(code)
|
||||
if len(code) == 0 {
|
||||
log.Printf("runner finished with no remaining code")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// WriteResult writes the output of the last vm execution to the given writer.
|
||||
//
|
||||
// Fails if
|
||||
// - required data inputs to the template are not available.
|
||||
// - the template for the given node point is note available for retrieval using the resource.Resource implementer.
|
||||
// - the supplied writer fails to process the writes.
|
||||
func(en *Engine) WriteResult(w io.Writer, ctx context.Context) error {
|
||||
r, err := en.vm.Render(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.WriteString(w, r)
|
||||
return err
|
||||
}
|
||||
141
engine/engine_test.go
Normal file
141
engine/engine_test.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"git.defalsify.org/festive/cache"
|
||||
"git.defalsify.org/festive/resource"
|
||||
"git.defalsify.org/festive/state"
|
||||
"git.defalsify.org/festive/testdata"
|
||||
)
|
||||
|
||||
var (
|
||||
dataGenerated bool = false
|
||||
dataDir string = testdata.DataDir
|
||||
)
|
||||
|
||||
type FsWrapper struct {
|
||||
*resource.FsResource
|
||||
st *state.State
|
||||
}
|
||||
|
||||
func NewFsWrapper(path string, st *state.State) FsWrapper {
|
||||
rs := resource.NewFsResource(path)
|
||||
return FsWrapper {
|
||||
&rs,
|
||||
st,
|
||||
}
|
||||
}
|
||||
|
||||
func(fs FsWrapper) one(sym string, ctx context.Context) (string, error) {
|
||||
return "one", nil
|
||||
}
|
||||
|
||||
func(fs FsWrapper) inky(sym string, ctx context.Context) (string, error) {
|
||||
return "tinkywinky", nil
|
||||
}
|
||||
|
||||
func(fs FsWrapper) FuncFor(sym string) (resource.EntryFunc, error) {
|
||||
switch sym {
|
||||
case "one":
|
||||
return fs.one, nil
|
||||
case "inky":
|
||||
return fs.inky, nil
|
||||
}
|
||||
return nil, fmt.Errorf("function for %v not found", sym)
|
||||
}
|
||||
|
||||
func(fs FsWrapper) GetCode(sym string) ([]byte, error) {
|
||||
sym += ".bin"
|
||||
fp := path.Join(fs.Path, sym)
|
||||
r, err := ioutil.ReadFile(fp)
|
||||
return r, err
|
||||
}
|
||||
|
||||
func generateTestData(t *testing.T) {
|
||||
if dataGenerated {
|
||||
return
|
||||
}
|
||||
var err error
|
||||
dataDir, err = testdata.Generate()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngineInit(t *testing.T) {
|
||||
generateTestData(t)
|
||||
ctx := context.TODO()
|
||||
st := state.NewState(17)
|
||||
rs := NewFsWrapper(dataDir, &st)
|
||||
ca := cache.NewCache().WithCacheSize(1024)
|
||||
|
||||
en := NewEngine(Config{}, &st, &rs, ca)
|
||||
err := en.Init("root", ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
w := bytes.NewBuffer(nil)
|
||||
err = en.WriteResult(w, ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := w.Bytes()
|
||||
expect_str := `hello world
|
||||
1:do the foo
|
||||
2:go to the bar`
|
||||
|
||||
if !bytes.Equal(b, []byte(expect_str)) {
|
||||
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect_str, b)
|
||||
}
|
||||
|
||||
input := []byte("1")
|
||||
_, err = en.Exec(input, ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r, _ := st.Where()
|
||||
if r != "foo" {
|
||||
t.Fatalf("expected where-string 'foo', got %s", r)
|
||||
}
|
||||
w = bytes.NewBuffer(nil)
|
||||
err = en.WriteResult(w, ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b = w.Bytes()
|
||||
expect := `this is in foo
|
||||
|
||||
it has more lines
|
||||
0:to foo
|
||||
1:go bar
|
||||
2:see long`
|
||||
|
||||
if !bytes.Equal(b, []byte(expect)) {
|
||||
t.Fatalf("expected\n\t%s\ngot:\n\t%s\n", expect, b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngineExecInvalidInput(t *testing.T) {
|
||||
generateTestData(t)
|
||||
ctx := context.TODO()
|
||||
st := state.NewState(17)
|
||||
rs := NewFsWrapper(dataDir, &st)
|
||||
ca := cache.NewCache().WithCacheSize(1024)
|
||||
|
||||
en := NewEngine(Config{}, &st, &rs, ca)
|
||||
err := en.Init("root", ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = en.Exec([]byte("_foo"), ctx)
|
||||
if err == nil {
|
||||
t.Fatalf("expected fail on invalid input")
|
||||
}
|
||||
}
|
||||
57
engine/loop.go
Normal file
57
engine/loop.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Loop starts an engine execution loop with the given symbol as the starting node.
|
||||
//
|
||||
// The root reads inputs from the provided reader, one line at a time.
|
||||
//
|
||||
// It will execute until running out of bytecode in the buffer.
|
||||
//
|
||||
// Any error not handled by the engine will terminate the oop and return an error.
|
||||
//
|
||||
// Rendered output is written to the provided writer.
|
||||
func Loop(en *Engine, startSym string, ctx context.Context, reader io.Reader, writer io.Writer) error {
|
||||
err := en.Init(startSym, ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot init: %v\n", err)
|
||||
}
|
||||
|
||||
err = en.WriteResult(writer, ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writer.Write([]byte{0x0a})
|
||||
|
||||
running := true
|
||||
bufReader := bufio.NewReader(reader)
|
||||
for running {
|
||||
in, err := bufReader.ReadString('\n')
|
||||
if err == io.EOF {
|
||||
log.Printf("EOF found, that's all folks")
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot read input: %v\n", err)
|
||||
}
|
||||
in = strings.TrimSpace(in)
|
||||
running, err = en.Exec([]byte(in), ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unexpected termination: %v\n", err)
|
||||
}
|
||||
err = en.WriteResult(writer, ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writer.Write([]byte{0x0a})
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
119
engine/loop_test.go
Normal file
119
engine/loop_test.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.defalsify.org/festive/cache"
|
||||
"git.defalsify.org/festive/resource"
|
||||
"git.defalsify.org/festive/state"
|
||||
)
|
||||
|
||||
func TestLoopTop(t *testing.T) {
|
||||
generateTestData(t)
|
||||
ctx := context.TODO()
|
||||
st := state.NewState(0)
|
||||
rs := resource.NewFsResource(dataDir)
|
||||
ca := cache.NewCache().WithCacheSize(1024)
|
||||
|
||||
en := NewEngine(Config{}, &st, &rs, ca)
|
||||
err := en.Init("root", ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
input := []string{
|
||||
"2",
|
||||
"j",
|
||||
"1",
|
||||
}
|
||||
inputStr := strings.Join(input, "\n")
|
||||
inputBuf := bytes.NewBuffer(append([]byte(inputStr), 0x0a))
|
||||
outputBuf := bytes.NewBuffer(nil)
|
||||
log.Printf("running with input: %s", inputBuf.Bytes())
|
||||
|
||||
err = Loop(&en, "root", ctx, inputBuf, outputBuf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
location, _ := st.Where()
|
||||
if location != "foo" {
|
||||
fmt.Errorf("expected location 'foo', got %s", location)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoopBackForth(t *testing.T) {
|
||||
generateTestData(t)
|
||||
ctx := context.TODO()
|
||||
st := state.NewState(0)
|
||||
rs := resource.NewFsResource(dataDir)
|
||||
ca := cache.NewCache().WithCacheSize(1024)
|
||||
|
||||
en := NewEngine(Config{}, &st, &rs, ca)
|
||||
err := en.Init("root", ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
input := []string{
|
||||
"1",
|
||||
"0",
|
||||
"1",
|
||||
"0",
|
||||
}
|
||||
inputStr := strings.Join(input, "\n")
|
||||
inputBuf := bytes.NewBuffer(append([]byte(inputStr), 0x0a))
|
||||
outputBuf := bytes.NewBuffer(nil)
|
||||
log.Printf("running with input: %s", inputBuf.Bytes())
|
||||
|
||||
err = Loop(&en, "root", ctx, inputBuf, outputBuf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoopBrowse(t *testing.T) {
|
||||
generateTestData(t)
|
||||
ctx := context.TODO()
|
||||
st := state.NewState(0)
|
||||
rs := resource.NewFsResource(dataDir)
|
||||
ca := cache.NewCache().WithCacheSize(1024)
|
||||
|
||||
cfg := Config{
|
||||
OutputSize: 68,
|
||||
}
|
||||
en := NewEngine(cfg, &st, &rs, ca)
|
||||
err := en.Init("root", ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
input := []string{
|
||||
"1",
|
||||
"2",
|
||||
"00",
|
||||
"11",
|
||||
"00",
|
||||
}
|
||||
inputStr := strings.Join(input, "\n")
|
||||
inputBuf := bytes.NewBuffer(append([]byte(inputStr), 0x0a))
|
||||
outputBuf := bytes.NewBuffer(nil)
|
||||
log.Printf("running with input: %s", inputBuf.Bytes())
|
||||
|
||||
err = Loop(&en, "root", ctx, inputBuf, outputBuf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
location, idx := st.Where()
|
||||
if location != "long" {
|
||||
fmt.Errorf("expected location 'long', got %s", location)
|
||||
}
|
||||
if idx != 1 {
|
||||
fmt.Errorf("expected idx 1, got %v", idx)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user