From 198e4a90877431907a71a255d02e386600682b77 Mon Sep 17 00:00:00 2001 From: Dan Kortschak Date: Thu, 18 Jan 2024 16:54:07 +1030 Subject: [PATCH] lib,mito: add support for runtime error source location reporting --- lib/errors.go | 55 +++++++++++++++++++ mito.go | 18 +++---- mito_bench_test.go | 120 ++++++++++++++++++++--------------------- testdata/serve_tls.txt | 2 +- 4 files changed, 125 insertions(+), 70 deletions(-) create mode 100644 lib/errors.go diff --git a/lib/errors.go b/lib/errors.go new file mode 100644 index 0000000..1a220d5 --- /dev/null +++ b/lib/errors.go @@ -0,0 +1,55 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package lib + +import ( + "fmt" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common" +) + +// DecoratedError implements error source location rendering. +type DecoratedError struct { + AST *cel.Ast + Err error +} + +func (e DecoratedError) Error() string { + if e.Err == nil { + return "" + } + type nodeIDer interface { + NodeID() int64 + } + nodeErr, ok := e.Err.(nodeIDer) + if !ok { + return e.Err.Error() + } + id := nodeErr.NodeID() + if id == 0 { + return fmt.Sprintf("%v: unset node id", e.Err) + } + if e.AST == nil { + return fmt.Sprintf("%v: node %d", e.Err, id) + } + loc := e.AST.NativeRep().SourceInfo().GetStartLocation(id) + errs := common.NewErrors(e.AST.Source()) + errs.ReportErrorAtID(id, loc, e.Err.Error()) + return errs.ToDisplayString() +} diff --git a/mito.go b/mito.go index 0607964..d31ce3b 100644 --- a/mito.go +++ b/mito.go @@ -298,41 +298,41 @@ func debug(tag string, value any) { } func eval(src, root string, input interface{}, libs ...cel.EnvOption) (string, any, error) { - prg, err := compile(src, root, libs...) + prg, ast, err := compile(src, root, libs...) if err != nil { return "", nil, fmt.Errorf("failed program instantiation: %v", err) } - return run(prg, false, input) + return run(prg, ast, false, input) } -func compile(src, root string, libs ...cel.EnvOption) (cel.Program, error) { +func compile(src, root string, libs ...cel.EnvOption) (cel.Program, *cel.Ast, error) { opts := append([]cel.EnvOption{ cel.Declarations(decls.NewVar(root, decls.Dyn)), }, libs...) env, err := cel.NewEnv(opts...) if err != nil { - return nil, fmt.Errorf("failed to create env: %v", err) + return nil, nil, fmt.Errorf("failed to create env: %v", err) } ast, iss := env.Compile(src) if iss.Err() != nil { - return nil, fmt.Errorf("failed compilation: %v", iss.Err()) + return nil, nil, fmt.Errorf("failed compilation: %v", iss.Err()) } prg, err := env.Program(ast) if err != nil { - return nil, fmt.Errorf("failed program instantiation: %v", err) + return nil, nil, fmt.Errorf("failed program instantiation: %v", err) } - return prg, nil + return prg, ast, nil } -func run(prg cel.Program, fast bool, input interface{}) (string, any, error) { +func run(prg cel.Program, ast *cel.Ast, fast bool, input interface{}) (string, any, error) { if input == nil { input = interpreter.EmptyActivation() } out, _, err := prg.Eval(input) if err != nil { - return "", nil, fmt.Errorf("failed eval: %v", err) + return "", nil, fmt.Errorf("failed eval: %v", lib.DecoratedError{AST: ast, Err: err}) } v, err := out.ConvertToNative(reflect.TypeOf(&structpb.Value{})) diff --git a/mito_bench_test.go b/mito_bench_test.go index eaae673..846e8dc 100644 --- a/mito_bench_test.go +++ b/mito_bench_test.go @@ -36,99 +36,99 @@ var ( var benchmarks = []struct { name string - setup func(*testing.B) (prg cel.Program, state any, err error) + setup func(*testing.B) (prg cel.Program, ast *cel.Ast, state any, err error) }{ // Self-contained. { name: "hello_world_static", - setup: func(b *testing.B) (cel.Program, any, error) { - prg, err := compile( + setup: func(b *testing.B) (cel.Program, *cel.Ast, any, error) { + prg, ast, err := compile( `"hello world"`, root, ) - return prg, nil, err + return prg, ast, nil, err }, }, { name: "hello_world_object_static", - setup: func(b *testing.B) (cel.Program, any, error) { - prg, err := compile( + setup: func(b *testing.B) (cel.Program, *cel.Ast, any, error) { + prg, ast, err := compile( `{"greeting":"hello world"}`, root, ) - return prg, nil, err + return prg, ast, nil, err }, }, { name: "nested_static", - setup: func(b *testing.B) (cel.Program, any, error) { - prg, err := compile( + setup: func(b *testing.B) (cel.Program, *cel.Ast, any, error) { + prg, ast, err := compile( `{"a":{"b":{"c":{"d":{"e":"f"}}}}}`, root, ) - return prg, nil, err + return prg, ast, nil, err }, }, { name: "encode_json_static", - setup: func(b *testing.B) (cel.Program, any, error) { - prg, err := compile( + setup: func(b *testing.B) (cel.Program, *cel.Ast, any, error) { + prg, ast, err := compile( `{"a":{"b":{"c":{"d":{"e":"f"}}}}}.encode_json()`, root, lib.JSON(nil), ) - return prg, nil, err + return prg, ast, nil, err }, }, { name: "nested_collate_static", - setup: func(b *testing.B) (cel.Program, any, error) { - prg, err := compile( + setup: func(b *testing.B) (cel.Program, *cel.Ast, any, error) { + prg, ast, err := compile( `{"a":{"b":{"c":{"d":{"e":"f"}}}}}.collate("a.b.c.d.e")`, root, lib.Collections(), ) - return prg, nil, err + return prg, ast, nil, err }, }, // From state. { name: "hello_world_state", - setup: func(b *testing.B) (cel.Program, any, error) { - prg, err := compile(root, root) + setup: func(b *testing.B) (cel.Program, *cel.Ast, any, error) { + prg, ast, err := compile(root, root) state := map[string]any{root: "hello world"} - return prg, state, err + return prg, ast, state, err }, }, { name: "hello_world_object_state", - setup: func(b *testing.B) (cel.Program, any, error) { - prg, err := compile( + setup: func(b *testing.B) (cel.Program, *cel.Ast, any, error) { + prg, ast, err := compile( `{"greeting":state.greeting}`, root, ) state := map[string]any{root: mustParseJSON(`{"greeting": "hello world}"}`)} - return prg, state, err + return prg, ast, state, err }, }, { name: "nested_state", - setup: func(b *testing.B) (cel.Program, any, error) { - prg, err := compile(root, root) + setup: func(b *testing.B) (cel.Program, *cel.Ast, any, error) { + prg, ast, err := compile(root, root) state := map[string]any{root: mustParseJSON(`{"a":{"b":{"c":{"d":{"e":"f"}}}}}`)} - return prg, state, err + return prg, ast, state, err }, }, { name: "encode_json_state", - setup: func(b *testing.B) (cel.Program, any, error) { - prg, err := compile(`state.encode_json()`, + setup: func(b *testing.B) (cel.Program, *cel.Ast, any, error) { + prg, ast, err := compile(`state.encode_json()`, root, lib.JSON(nil), ) state := map[string]any{root: mustParseJSON(`{"a":{"b":{"c":{"d":{"e":"f"}}}}}`)} - return prg, state, err + return prg, ast, state, err }, }, // These two have additional complexity due to a wart that requires @@ -140,24 +140,24 @@ var benchmarks = []struct { // Similar in the net versions below. { name: "nested_collate_list_state", - setup: func(b *testing.B) (cel.Program, any, error) { - prg, err := compile(`[state].collate("a.b.c.d.e")`, + setup: func(b *testing.B) (cel.Program, *cel.Ast, any, error) { + prg, ast, err := compile(`[state].collate("a.b.c.d.e")`, root, lib.Collections(), ) state := map[string]any{root: mustParseJSON(`{"a":{"b":{"c":{"d":{"e":"f"}}}}}`)} - return prg, state, err + return prg, ast, state, err }, }, { name: "nested_collate_map_state", - setup: func(b *testing.B) (cel.Program, any, error) { - prg, err := compile(`{"state": state}.collate("state.a.b.c.d.e")`, + setup: func(b *testing.B) (cel.Program, *cel.Ast, any, error) { + prg, ast, err := compile(`{"state": state}.collate("state.a.b.c.d.e")`, root, lib.Collections(), ) state := map[string]any{root: mustParseJSON(`{"a":{"b":{"c":{"d":{"e":"f"}}}}}`)} - return prg, state, err + return prg, ast, state, err }, }, @@ -167,132 +167,132 @@ var benchmarks = []struct { // This is to get an idea of how much of the bench work is coming from // the test server. name: "null_net", - setup: func(b *testing.B) (cel.Program, any, error) { + setup: func(b *testing.B) (cel.Program, *cel.Ast, any, error) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusOK) })) b.Cleanup(func() { srv.Close() }) - prg, err := compile( + prg, ast, err := compile( fmt.Sprintf(`get(%q).size()`, srv.URL), root, lib.HTTP(srv.Client(), nil, nil), ) - return prg, nil, err + return prg, ast, nil, err }, }, { name: "hello_world_net", - setup: func(b *testing.B) (cel.Program, any, error) { + setup: func(b *testing.B) (cel.Program, *cel.Ast, any, error) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("hello world")) })) b.Cleanup(func() { srv.Close() }) - prg, err := compile( + prg, ast, err := compile( fmt.Sprintf(`string(get(%q).Body)`, srv.URL), root, lib.HTTP(srv.Client(), nil, nil), ) - return prg, nil, err + return prg, ast, nil, err }, }, { name: "hello_world_object_net", - setup: func(b *testing.B) (cel.Program, any, error) { + setup: func(b *testing.B) (cel.Program, *cel.Ast, any, error) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Write([]byte(`{"greeting":"hello world"}`)) })) b.Cleanup(func() { srv.Close() }) - prg, err := compile( + prg, ast, err := compile( fmt.Sprintf(`{"greeting":bytes(get(%q).Body).decode_json().greeting}`, srv.URL), root, lib.HTTP(srv.Client(), nil, nil), lib.JSON(nil), ) - return prg, nil, err + return prg, ast, nil, err }, }, { name: "nested_net", - setup: func(b *testing.B) (cel.Program, any, error) { + setup: func(b *testing.B) (cel.Program, *cel.Ast, any, error) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Write([]byte(`{"a":{"b":{"c":{"d":{"e":"f"}}}}}`)) })) b.Cleanup(func() { srv.Close() }) - prg, err := compile( + prg, ast, err := compile( fmt.Sprintf(`bytes(get(%q).Body).decode_json()`, srv.URL), root, lib.HTTP(srv.Client(), nil, nil), lib.JSON(nil), ) - return prg, nil, err + return prg, ast, nil, err }, }, { name: "encode_json_null_net", - setup: func(b *testing.B) (cel.Program, any, error) { + setup: func(b *testing.B) (cel.Program, *cel.Ast, any, error) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Write([]byte(`{"a":{"b":{"c":{"d":{"e":"f"}}}}}`)) })) b.Cleanup(func() { srv.Close() }) - prg, err := compile( + prg, ast, err := compile( fmt.Sprintf(`get(%q).Body`, srv.URL), root, lib.HTTP(srv.Client(), nil, nil), lib.JSON(nil), ) - return prg, nil, err + return prg, ast, nil, err }, }, { // encode_json_net should bead assessed with reference to encode_json_null_net // which performs the same request but does not round-trip the object. name: "encode_json_net", - setup: func(b *testing.B) (cel.Program, any, error) { + setup: func(b *testing.B) (cel.Program, *cel.Ast, any, error) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Write([]byte(`{"a":{"b":{"c":{"d":{"e":"f"}}}}}`)) })) b.Cleanup(func() { srv.Close() }) - prg, err := compile( + prg, ast, err := compile( fmt.Sprintf(`bytes(get(%q).Body).decode_json().encode_json()`, srv.URL), root, lib.HTTP(srv.Client(), nil, nil), lib.JSON(nil), ) - return prg, nil, err + return prg, ast, nil, err }, }, { name: "nested_collate_list_net", - setup: func(b *testing.B) (cel.Program, any, error) { + setup: func(b *testing.B) (cel.Program, *cel.Ast, any, error) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Write([]byte(`{"a":{"b":{"c":{"d":{"e":"f"}}}}}`)) })) b.Cleanup(func() { srv.Close() }) - prg, err := compile( + prg, ast, err := compile( fmt.Sprintf(`[bytes(get(%q).Body).decode_json()].collate("a.b.c.d.e")`, srv.URL), root, lib.HTTP(srv.Client(), nil, nil), lib.JSON(nil), lib.Collections(), ) - return prg, nil, err + return prg, ast, nil, err }, }, { name: "nested_collate_map_net", - setup: func(b *testing.B) (cel.Program, any, error) { + setup: func(b *testing.B) (cel.Program, *cel.Ast, any, error) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Write([]byte(`{"a":{"b":{"c":{"d":{"e":"f"}}}}}`)) })) b.Cleanup(func() { srv.Close() }) - prg, err := compile( + prg, ast, err := compile( fmt.Sprintf(`{"body": bytes(get(%q).Body).decode_json()}.collate("body.a.b.c.d.e")`, srv.URL), root, lib.HTTP(srv.Client(), nil, nil), lib.JSON(nil), lib.Collections(), ) - return prg, nil, err + return prg, ast, nil, err }, }, } @@ -302,14 +302,14 @@ func BenchmarkMito(b *testing.B) { sampled := false b.Run(bench.name, func(b *testing.B) { b.StopTimer() - prg, state, err := bench.setup(b) + prg, ast, state, err := bench.setup(b) if err != nil { b.Fatalf("failed setup: %v", err) } b.StartTimer() for i := 0; i < b.N; i++ { - v, _, err := run(prg, *fastMarshal, state) + v, _, err := run(prg, ast, *fastMarshal, state) if err != nil { b.Fatalf("failed operation: %v", err) } diff --git a/testdata/serve_tls.txt b/testdata/serve_tls.txt index 3469b53..34e7cdb 100644 --- a/testdata/serve_tls.txt +++ b/testdata/serve_tls.txt @@ -4,7 +4,7 @@ cmpenv src.cel src_var.cel ! mito -use http src.cel ! stdout . -stderr 'failed eval: Get "https://127.0.0.1:[0-9]{1,5}": (?:tls: failed to verify certificate: )?x509: (?:certificate signed by unknown authority|.*certificate is not trusted)' +stderr 'failed eval: ERROR: :2:62: Get "https://127.0.0.1:[0-9]{1,5}": (?:tls: failed to verify certificate: )?x509: (?:certificate signed by unknown authority|.*certificate is not trusted)' mito -use http -insecure src.cel cmp stdout want_insecure.txt