go-vise

Constrained Size Output Virtual Machine
Info | Log | Files | Refs | README | LICENSE

commit 67babc58038077f9bf5767532a24bcd6e2543555
parent d898a995014e183ff41c690021cb1af0a2a9e114
Author: lash <dev@holbrook.no>
Date:   Sun, 23 Apr 2023 08:33:34 +0100

Add error display capability in page render

Diffstat:
Mrender/page.go | 30++++++++++++++++++++++++++++++
Mvm/input.go | 12++++++++++++
Mvm/runner.go | 40+++++++++++++++++++++++++++++++++++++---
Mvm/runner_test.go | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
4 files changed, 141 insertions(+), 5 deletions(-)

diff --git a/render/page.go b/render/page.go @@ -19,6 +19,8 @@ type Page struct { menu *Menu // Menu rendererer. sink *string // Content symbol rendered by dynamic size. sizer *Sizer // Process size constraints. + err error // Error state to prepend to output. + errConst error // Use this error for display on all errors. } // NewPage creates a new Page object. @@ -48,6 +50,26 @@ func(pg *Page) WithSizer(sizer *Sizer) *Page { return pg } +// WithError adds an error to prepend to the page output. +func(pg *Page) WithError(err error) *Page { + pg.err = err + return pg +} + +func(pg *Page) WithOpaqueError(err error) *Page { + pg.errConst = err + return pg +} + +func(pg *Page) Error() string { + if pg.err != nil { + if pg.errConst != nil { + return pg.errConst.Error() + } + } + return pg.err.Error() +} + // Usage returns size used by values and menu, and remaining size available func(pg *Page) Usage() (uint32, uint32, error) { var l int @@ -135,6 +157,14 @@ func(pg *Page) RenderTemplate(sym string, values map[string]string, idx uint16, if err != nil { return "", err } + if pg.err != nil { + Logg.DebugCtxf(ctx, "Prepending error: %s", pg.err) + if len(tpl) == 0 { + tpl = pg.err.Error() + } else { + tpl = fmt.Sprintf("%s\n%s", pg.err, tpl) + } + } if pg.sizer != nil { values, err = pg.sizer.GetAt(values, idx) if err != nil { diff --git a/vm/input.go b/vm/input.go @@ -20,6 +20,18 @@ var ( ) +type InvalidInputError struct { + input string +} + +func NewInvalidInputError(input string) error { + return InvalidInputError{input} +} + +func(e InvalidInputError) Error() string { + return "invalid input" +} + // CheckInput validates the given byte string as client input. func ValidInput(input []byte) error { if !inputRegex.Match(input) { diff --git a/vm/runner.go b/vm/runner.go @@ -10,6 +10,21 @@ import ( "git.defalsify.org/vise.git/state" ) +type ExternalCodeError struct { + sym string + err error +} + +func NewExternalCodeError(sym string, err error) error { + return ExternalCodeError{sym, err} +} + +func(e ExternalCodeError) Error() string { + return fmt.Sprintf("[%s] %v", e.sym, e.err) +} + + + // Vm holds sub-components mutated by the vm execution. // TODO: Renderer should be passed to avoid proxy methods not strictly related to vm operation type Vm struct { @@ -129,6 +144,7 @@ func(vm *Vm) Run(b []byte, ctx context.Context) ([]byte, error) { default: err = fmt.Errorf("Unhandled state: %v", op) } + b, err = vm.RunErrCheck(ctx, b, err) if err != nil { return b, err } @@ -145,6 +161,24 @@ func(vm *Vm) Run(b []byte, ctx context.Context) ([]byte, error) { return b, nil } +func(vm *Vm) RunErrCheck(ctx context.Context, b []byte, err error) ([]byte, error) { + if err == nil { + return b, err + } + vm.pg = vm.pg.WithError(err) + + v, err := vm.st.MatchFlag(state.FLAG_LOADFAIL, false) + if err != nil { + panic(err) + } + if !v { + return b, err + } + + b = NewLine(nil, MOVE, []string{"_catch"}, nil, nil) + return b, nil +} + // RunDeadCheck determines whether a state of empty bytecode should result in termination. // // If there is remaining bytecode, this method is a noop. @@ -183,6 +217,8 @@ func(vm *Vm) RunDeadCheck(b []byte, ctx context.Context) ([]byte, error) { if location == "" { return b, fmt.Errorf("dead runner with no current location") } + cerr := fmt.Errorf("invalid input") + vm.pg.WithError(cerr) b = NewLine(nil, MOVE, []string{"_catch"}, nil, nil) return b, nil } @@ -479,13 +515,12 @@ func(vm *Vm) refresh(key string, rs resource.Resource, ctx context.Context) (str input, _ := vm.st.GetInput() r, err := fn(key, input, ctx) if err != nil { - Logg.TraceCtxf(ctx, "loadfail", "err", err) var perr error _, perr = vm.st.SetFlag(state.FLAG_LOADFAIL) if perr != nil { panic(err) } - return "", err + return "", NewExternalCodeError(key, err) } for _, flag := range r.FlagSet { if !state.IsWriteableFlag(flag) { @@ -513,7 +548,6 @@ func(vm *Vm) refresh(key string, rs resource.Resource, ctx context.Context) (str if haveLang { vm.st.SetLanguage(r.Content) } - return r.Content, err } diff --git a/vm/runner_test.go b/vm/runner_test.go @@ -19,6 +19,7 @@ type TestResource struct { resource.MenuResource state *state.State RootCode []byte + CatchContent string } func getOne(sym string, input []byte, ctx context.Context) (resource.Result, error) { @@ -46,6 +47,11 @@ func getEcho(sym string, input []byte, ctx context.Context) (resource.Result, er }, nil } + +func uhOh(sym string, input []byte, ctx context.Context) (resource.Result, error) { + return resource.Result{}, fmt.Errorf("uh-oh spaghetti'ohs") +} + func setFlag(sym string, input []byte, ctx context.Context) (resource.Result, error) { s := fmt.Sprintf("ping") r := resource.Result{ @@ -86,13 +92,12 @@ func (r TestResource) GetTemplate(sym string, ctx context.Context) (string, erro case "root": return "root", nil case "_catch": - return "aiee", nil + return r.CatchContent, nil case "ouf": return "ouch", nil case "flagCatch": return "flagiee", nil } - panic(fmt.Sprintf("unknown symbol %s", sym)) return "", fmt.Errorf("unknown symbol %s", sym) } @@ -112,6 +117,8 @@ func (r TestResource) FuncFor(sym string) (resource.EntryFunc, error) { return setFlag, nil case "set_lang": return set_lang, nil + case "aiee": + return uhOh, nil } return nil, fmt.Errorf("invalid function: '%s'", sym) } @@ -660,3 +667,56 @@ func TestSetLang(t *testing.T) { t.Fatalf("expected language 'nor',, got %s", lang.Code) } } + +func TestLoadError(t *testing.T) { + st := state.NewState(0).WithDebug() + rs := TestResource{} + ca := cache.NewCache() + vm := NewVm(&st, &rs, ca, nil) + + st.Down("root") + st.SetInput([]byte{}) + b := NewLine(nil, LOAD, []string{"aiee"}, []byte{0x01, 0x10}, nil) + b = NewLine(b, HALT, nil, nil, nil) + + var err error + ctx := context.TODO() + b, err = vm.Run(b, ctx) + if err != nil { + t.Fatal(err) + } + + r, err := vm.Render(ctx) + if err != nil { + t.Fatal(err) + } + expect := `[aiee] uh-oh spaghetti'ohs +0:repent` + if r != expect { + t.Fatalf("expected: \n\t%s\ngot:\n\t%s", expect, r) + } + + rs.CatchContent = "foo" + + st.Up() + st.SetInput([]byte{}) + b = NewLine(nil, LOAD, []string{"aiee"}, []byte{0x01, 0x10}, nil) + b = NewLine(b, HALT, nil, nil, nil) + + b, err = vm.Run(b, ctx) + if err != nil { + t.Fatal(err) + } + + r, err = vm.Render(ctx) + if err != nil { + t.Fatal(err) + } + expect = `[aiee] uh-oh spaghetti'ohs +foo +0:repent` + if r != expect { + t.Fatalf("expected: \n\t%s\ngot:\n\t%s", expect, r) + } + +}