From dd9c61831dd5bc513b5bb98946cbc7ca4faf3b88 Mon Sep 17 00:00:00 2001 From: SimFG Date: Fri, 22 Dec 2023 18:36:44 +0800 Subject: [PATCH] enhance: Support to get the param value in the runtime (#29297) /kind improvement issue: #29299 Signed-off-by: SimFG --- cmd/roles/roles.go | 3 + go.mod | 2 + go.sum | 2 + internal/datacoord/server.go | 2 + internal/datanode/data_node.go | 2 + internal/distributed/datanode/service.go | 1 - internal/http/router.go | 3 + internal/http/server.go | 16 +++++ internal/http/server_test.go | 26 ++++++++ internal/indexnode/indexnode.go | 2 + internal/proxy/proxy.go | 2 + internal/querycoordv2/server.go | 2 + internal/querynodev2/server.go | 2 + internal/rootcoord/root_coord.go | 2 + pkg/go.mod | 4 +- pkg/go.sum | 6 +- pkg/util/expr/expr.go | 80 ++++++++++++++++++++++++ pkg/util/expr/expr_test.go | 63 +++++++++++++++++++ 18 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 pkg/util/expr/expr.go create mode 100644 pkg/util/expr/expr_test.go diff --git a/cmd/roles/roles.go b/cmd/roles/roles.go index f4685bf2d8..9475d03b84 100644 --- a/cmd/roles/roles.go +++ b/cmd/roles/roles.go @@ -42,6 +42,7 @@ import ( "github.com/milvus-io/milvus/pkg/metrics" "github.com/milvus-io/milvus/pkg/tracer" "github.com/milvus-io/milvus/pkg/util/etcd" + "github.com/milvus-io/milvus/pkg/util/expr" "github.com/milvus-io/milvus/pkg/util/generic" "github.com/milvus-io/milvus/pkg/util/logutil" "github.com/milvus-io/milvus/pkg/util/metricsinfo" @@ -339,6 +340,8 @@ func (mr *MilvusRoles) Run() { paramtable.Init() } + expr.Init() + expr.Register("param", paramtable.Get()) http.ServeHTTP() setupPrometheusHTTPServer(Registry) diff --git a/go.mod b/go.mod index b93efd8f49..e808fb6019 100644 --- a/go.mod +++ b/go.mod @@ -104,6 +104,7 @@ require ( github.com/docker/go-units v0.4.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.5.0 // indirect + github.com/expr-lang/expr v1.15.7 // indirect github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c // indirect github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect @@ -241,6 +242,7 @@ require ( replace ( github.com/apache/pulsar-client-go => github.com/milvus-io/pulsar-client-go v0.6.10 github.com/bketelsen/crypt => github.com/bketelsen/crypt v0.0.4 // Fix security alert for core-os/etcd + github.com/expr-lang/expr => github.com/SimFG/expr v0.0.0-20231218130003-94d085776dc5 github.com/go-kit/kit => github.com/go-kit/kit v0.1.0 github.com/milvus-io/milvus/pkg => ./pkg github.com/streamnative/pulsarctl => github.com/xiaofan-luan/pulsarctl v0.5.1 diff --git a/go.sum b/go.sum index 47b801aa4b..73c0ca4ac2 100644 --- a/go.sum +++ b/go.sum @@ -74,6 +74,8 @@ github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1 github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= +github.com/SimFG/expr v0.0.0-20231218130003-94d085776dc5 h1:U2V21xTXzCo7RpB1DHpc2X0SToiy/4PuZ/gEYd5/ytY= +github.com/SimFG/expr v0.0.0-20231218130003-94d085776dc5/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ= github.com/actgardner/gogen-avro/v10 v10.1.0/go.mod h1:o+ybmVjEa27AAr35FRqU98DJu1fXES56uXniYFv4yDA= github.com/actgardner/gogen-avro/v10 v10.2.1/go.mod h1:QUhjeHPchheYmMDni/Nx7VB0RsT/ee8YIgGY/xpEQgQ= github.com/actgardner/gogen-avro/v9 v9.1.0/go.mod h1:nyTj6wPqDJoxM3qdnjcLv+EnMDSDFqE0qDpva2QRmKc= diff --git a/internal/datacoord/server.go b/internal/datacoord/server.go index f39b62dac8..76525dbd64 100644 --- a/internal/datacoord/server.go +++ b/internal/datacoord/server.go @@ -52,6 +52,7 @@ import ( "github.com/milvus-io/milvus/pkg/mq/msgstream" "github.com/milvus-io/milvus/pkg/mq/msgstream/mqwrapper" "github.com/milvus-io/milvus/pkg/util" + "github.com/milvus-io/milvus/pkg/util/expr" "github.com/milvus-io/milvus/pkg/util/funcutil" "github.com/milvus-io/milvus/pkg/util/logutil" "github.com/milvus-io/milvus/pkg/util/merr" @@ -227,6 +228,7 @@ func CreateServer(ctx context.Context, factory dependency.Factory, opts ...Optio for _, opt := range opts { opt(s) } + expr.Register("datacoord", s) return s } diff --git a/internal/datanode/data_node.go b/internal/datanode/data_node.go index 5b89d835f0..a6c90ba05f 100644 --- a/internal/datanode/data_node.go +++ b/internal/datanode/data_node.go @@ -48,6 +48,7 @@ import ( "github.com/milvus-io/milvus/pkg/log" "github.com/milvus-io/milvus/pkg/metrics" "github.com/milvus-io/milvus/pkg/mq/msgdispatcher" + "github.com/milvus-io/milvus/pkg/util/expr" "github.com/milvus-io/milvus/pkg/util/logutil" "github.com/milvus-io/milvus/pkg/util/metricsinfo" "github.com/milvus-io/milvus/pkg/util/paramtable" @@ -144,6 +145,7 @@ func NewDataNode(ctx context.Context, factory dependency.Factory) *DataNode { reportImportRetryTimes: 10, } node.UpdateStateCode(commonpb.StateCode_Abnormal) + expr.Register("datanode", node) return node } diff --git a/internal/distributed/datanode/service.go b/internal/distributed/datanode/service.go index 81504253f4..3efcc56400 100644 --- a/internal/distributed/datanode/service.go +++ b/internal/distributed/datanode/service.go @@ -91,7 +91,6 @@ func NewServer(ctx context.Context, factory dependency.Factory) (*Server, error) } s.datanode = dn.NewDataNode(s.ctx, s.factory) - return s, nil } diff --git a/internal/http/router.go b/internal/http/router.go index 99fca2526e..220ed3d5f7 100644 --- a/internal/http/router.go +++ b/internal/http/router.go @@ -24,3 +24,6 @@ const LogLevelRouterPath = "/log/level" // EventLogRouterPath is path for eventlog control. const EventLogRouterPath = "/eventlog" + +// ExprPath is path for expression. +const ExprPath = "/expr" diff --git a/internal/http/server.go b/internal/http/server.go index f99a481001..849767b366 100644 --- a/internal/http/server.go +++ b/internal/http/server.go @@ -28,6 +28,7 @@ import ( "github.com/milvus-io/milvus/internal/http/healthz" "github.com/milvus-io/milvus/pkg/eventlog" "github.com/milvus-io/milvus/pkg/log" + "github.com/milvus-io/milvus/pkg/util/expr" "github.com/milvus-io/milvus/pkg/util/paramtable" ) @@ -70,6 +71,21 @@ func registerDefaults() { Path: EventLogRouterPath, Handler: eventlog.Handler(), }) + Register(&Handler{ + Path: ExprPath, + Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + code := req.URL.Query().Get("code") + auth := req.URL.Query().Get("auth") + output, err := expr.Exec(code, auth) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf(`{"msg": "failed to execute expression, %s"}`, err.Error()))) + return + } + w.WriteHeader(http.StatusOK) + w.Write([]byte(fmt.Sprintf(`{"output": "%s"}`, output))) + }), + }) } func Register(h *Handler) { diff --git a/internal/http/server_test.go b/internal/http/server_test.go index cc838d6921..d68a38d2d4 100644 --- a/internal/http/server_test.go +++ b/internal/http/server_test.go @@ -35,6 +35,7 @@ import ( "github.com/milvus-io/milvus-proto/go-api/v2/commonpb" "github.com/milvus-io/milvus/internal/http/healthz" "github.com/milvus-io/milvus/pkg/log" + "github.com/milvus-io/milvus/pkg/util/expr" "github.com/milvus-io/milvus/pkg/util/paramtable" ) @@ -192,6 +193,31 @@ func (suite *HTTPServerTestSuite) TestPprofHandler() { } } +func (suite *HTTPServerTestSuite) TestExprHandler() { + expr.Init() + expr.Register("foo", "hello") + suite.Run("fail", func() { + url := "http://localhost:" + DefaultListenPort + ExprPath + "?code=foo" + client := http.Client{} + req, _ := http.NewRequest(http.MethodGet, url, nil) + resp, err := client.Do(req) + suite.Nil(err) + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + suite.True(strings.Contains(string(body), "failed to execute")) + }) + suite.Run("success", func() { + url := "http://localhost:" + DefaultListenPort + ExprPath + "?auth=by-dev&code=foo" + client := http.Client{} + req, _ := http.NewRequest(http.MethodGet, url, nil) + resp, err := client.Do(req) + suite.Nil(err) + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + suite.True(strings.Contains(string(body), "hello")) + }) +} + func TestHTTPServerSuite(t *testing.T) { suite.Run(t, new(HTTPServerTestSuite)) } diff --git a/internal/indexnode/indexnode.go b/internal/indexnode/indexnode.go index 6185207d64..2a771a9049 100644 --- a/internal/indexnode/indexnode.go +++ b/internal/indexnode/indexnode.go @@ -53,6 +53,7 @@ import ( "github.com/milvus-io/milvus/pkg/common" "github.com/milvus-io/milvus/pkg/log" "github.com/milvus-io/milvus/pkg/metrics" + "github.com/milvus-io/milvus/pkg/util/expr" "github.com/milvus-io/milvus/pkg/util/hardware" "github.com/milvus-io/milvus/pkg/util/lifetime" "github.com/milvus-io/milvus/pkg/util/merr" @@ -126,6 +127,7 @@ func NewIndexNode(ctx context.Context, factory dependency.Factory) *IndexNode { sc := NewTaskScheduler(b.loopCtx) b.sched = sc + expr.Register("indexnode", b) return b } diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index b537287425..287808dad7 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -43,6 +43,7 @@ import ( "github.com/milvus-io/milvus/pkg/metrics" "github.com/milvus-io/milvus/pkg/mq/msgstream" "github.com/milvus-io/milvus/pkg/util/commonpbutil" + "github.com/milvus-io/milvus/pkg/util/expr" "github.com/milvus-io/milvus/pkg/util/logutil" "github.com/milvus-io/milvus/pkg/util/metricsinfo" "github.com/milvus-io/milvus/pkg/util/paramtable" @@ -144,6 +145,7 @@ func NewProxy(ctx context.Context, factory dependency.Factory) (*Proxy, error) { replicateStreamManager: replicateStreamManager, } node.UpdateStateCode(commonpb.StateCode_Abnormal) + expr.Register("proxy", node) logutil.Logger(ctx).Debug("create a new Proxy instance", zap.Any("state", node.stateCode.Load())) return node, nil } diff --git a/internal/querycoordv2/server.go b/internal/querycoordv2/server.go index 95c0d62977..56924add2c 100644 --- a/internal/querycoordv2/server.go +++ b/internal/querycoordv2/server.go @@ -58,6 +58,7 @@ import ( "github.com/milvus-io/milvus/pkg/log" "github.com/milvus-io/milvus/pkg/metrics" "github.com/milvus-io/milvus/pkg/util" + "github.com/milvus-io/milvus/pkg/util/expr" "github.com/milvus-io/milvus/pkg/util/merr" "github.com/milvus-io/milvus/pkg/util/metricsinfo" "github.com/milvus-io/milvus/pkg/util/paramtable" @@ -135,6 +136,7 @@ func NewQueryCoord(ctx context.Context) (*Server, error) { } server.UpdateStateCode(commonpb.StateCode_Abnormal) server.queryNodeCreator = session.DefaultQueryNodeCreator + expr.Register("querycoord", server) return server, nil } diff --git a/internal/querynodev2/server.go b/internal/querynodev2/server.go index f367a60bea..418c561d6c 100644 --- a/internal/querynodev2/server.go +++ b/internal/querynodev2/server.go @@ -64,6 +64,7 @@ import ( "github.com/milvus-io/milvus/pkg/log" "github.com/milvus-io/milvus/pkg/metrics" "github.com/milvus-io/milvus/pkg/mq/msgdispatcher" + "github.com/milvus-io/milvus/pkg/util/expr" "github.com/milvus-io/milvus/pkg/util/gc" "github.com/milvus-io/milvus/pkg/util/hardware" "github.com/milvus-io/milvus/pkg/util/lifetime" @@ -142,6 +143,7 @@ func NewQueryNode(ctx context.Context, factory dependency.Factory) *QueryNode { } node.tSafeManager = tsafe.NewTSafeReplica() + expr.Register("querynode", node) return node } diff --git a/internal/rootcoord/root_coord.go b/internal/rootcoord/root_coord.go index 4f0e7fe1df..5c179d2f24 100644 --- a/internal/rootcoord/root_coord.go +++ b/internal/rootcoord/root_coord.go @@ -60,6 +60,7 @@ import ( "github.com/milvus-io/milvus/pkg/util" "github.com/milvus-io/milvus/pkg/util/commonpbutil" "github.com/milvus-io/milvus/pkg/util/crypto" + "github.com/milvus-io/milvus/pkg/util/expr" "github.com/milvus-io/milvus/pkg/util/funcutil" "github.com/milvus-io/milvus/pkg/util/logutil" "github.com/milvus-io/milvus/pkg/util/merr" @@ -147,6 +148,7 @@ func NewCore(c context.Context, factory dependency.Factory) (*Core, error) { core.UpdateStateCode(commonpb.StateCode_Abnormal) core.SetProxyCreator(proxyutil.DefaultProxyCreator) + expr.Register("rootcoord", core) return core, nil } diff --git a/pkg/go.mod b/pkg/go.mod index 93eb0b64cd..245cdde9d9 100644 --- a/pkg/go.mod +++ b/pkg/go.mod @@ -9,6 +9,7 @@ require ( github.com/cockroachdb/errors v1.9.1 github.com/confluentinc/confluent-kafka-go v1.9.1 github.com/containerd/cgroups v1.1.0 + github.com/expr-lang/expr v1.15.7 github.com/golang/protobuf v1.5.3 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/klauspost/compress v1.16.5 @@ -25,7 +26,7 @@ require ( github.com/spf13/cast v1.3.1 github.com/spf13/viper v1.8.1 github.com/streamnative/pulsarctl v0.5.0 - github.com/stretchr/testify v1.8.3 + github.com/stretchr/testify v1.8.4 github.com/tikv/client-go/v2 v2.0.4 github.com/uber/jaeger-client-go v2.30.0+incompatible go.etcd.io/etcd/client/v3 v3.5.5 @@ -171,6 +172,7 @@ require ( replace ( github.com/apache/pulsar-client-go => github.com/milvus-io/pulsar-client-go v0.6.10 github.com/bketelsen/crypt => github.com/bketelsen/crypt v0.0.4 // Fix security alert for core-os/etcd + github.com/expr-lang/expr => github.com/SimFG/expr v0.0.0-20231218130003-94d085776dc5 github.com/go-kit/kit => github.com/go-kit/kit v0.1.0 github.com/streamnative/pulsarctl => github.com/xiaofan-luan/pulsarctl v0.5.1 github.com/tecbot/gorocksdb => github.com/milvus-io/gorocksdb v0.0.0-20220624081344-8c5f4212846b // indirect diff --git a/pkg/go.sum b/pkg/go.sum index 5376060c9b..95976e7cd2 100644 --- a/pkg/go.sum +++ b/pkg/go.sum @@ -58,6 +58,8 @@ github.com/DataDog/zstd v1.5.0/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwS github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= +github.com/SimFG/expr v0.0.0-20231218130003-94d085776dc5 h1:U2V21xTXzCo7RpB1DHpc2X0SToiy/4PuZ/gEYd5/ytY= +github.com/SimFG/expr v0.0.0-20231218130003-94d085776dc5/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ= github.com/actgardner/gogen-avro/v10 v10.1.0/go.mod h1:o+ybmVjEa27AAr35FRqU98DJu1fXES56uXniYFv4yDA= github.com/actgardner/gogen-avro/v10 v10.2.1/go.mod h1:QUhjeHPchheYmMDni/Nx7VB0RsT/ee8YIgGY/xpEQgQ= github.com/actgardner/gogen-avro/v9 v9.1.0/go.mod h1:nyTj6wPqDJoxM3qdnjcLv+EnMDSDFqE0qDpva2QRmKc= @@ -687,8 +689,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= diff --git a/pkg/util/expr/expr.go b/pkg/util/expr/expr.go new file mode 100644 index 0000000000..b45fe0d6e3 --- /dev/null +++ b/pkg/util/expr/expr.go @@ -0,0 +1,80 @@ +/* + * Licensed to the LF AI & Data foundation under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 expr + +import ( + "fmt" + + "github.com/expr-lang/expr" + "github.com/expr-lang/expr/vm" + "go.uber.org/zap" + + "github.com/milvus-io/milvus/pkg/log" + "github.com/milvus-io/milvus/pkg/util/paramtable" +) + +var ( + v *vm.VM + env map[string]any + authKey string +) + +func Init() { + v = &vm.VM{} + env = make(map[string]any) + authKey = paramtable.Get().EtcdCfg.RootPath.GetValue() +} + +func Register(key string, value any) { + if env != nil { + env[key] = value + } +} + +func Exec(code, auth string) (res string, err error) { + defer func() { + if e := recover(); e != nil { + err = fmt.Errorf("panic: %v", e) + } + }() + if v == nil { + return "", fmt.Errorf("the expr isn't inited") + } + if code == "" { + return "", fmt.Errorf("the expr code is empty") + } + if auth == "" { + return "", fmt.Errorf("the expr auth is empty") + } + if authKey != auth { + return "", fmt.Errorf("the expr auth is invalid") + } + program, err := expr.Compile(code, expr.Env(env)) + if err != nil { + log.Warn("expr compile failed", zap.String("code", code), zap.Error(err)) + return "", err + } + + output, err := v.Run(program, env) + if err != nil { + log.Warn("expr run failed", zap.String("code", code), zap.Error(err)) + return "", err + } + return fmt.Sprintf("%v", output), nil +} diff --git a/pkg/util/expr/expr_test.go b/pkg/util/expr/expr_test.go new file mode 100644 index 0000000000..a08d76d660 --- /dev/null +++ b/pkg/util/expr/expr_test.go @@ -0,0 +1,63 @@ +/* + * Licensed to the LF AI & Data foundation under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 expr + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/milvus-io/milvus/pkg/util/paramtable" +) + +func TestExec(t *testing.T) { + paramtable.Init() + t.Run("not init", func(t *testing.T) { + _, err := Exec("1+1", "by-dev") + assert.Error(t, err) + }) + Init() + Register("foo", "hello") + + t.Run("empty code", func(t *testing.T) { + _, err := Exec("", "by-dev") + assert.Error(t, err) + }) + + t.Run("empty auth", func(t *testing.T) { + _, err := Exec("1+1", "") + assert.Error(t, err) + }) + + t.Run("invalid auth", func(t *testing.T) { + _, err := Exec("1+1", "000") + assert.Error(t, err) + }) + + t.Run("invalid code", func(t *testing.T) { + _, err := Exec("1+", "by-dev") + assert.Error(t, err) + }) + + t.Run("valid code", func(t *testing.T) { + out, err := Exec("foo", "by-dev") + assert.NoError(t, err) + assert.Equal(t, "hello", out) + }) +}