go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/starlark/interpreter/helpers_test.go (about) 1 // Copyright 2018 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package interpreter 16 17 // Code shared by tests. 18 19 import ( 20 "context" 21 "fmt" 22 "sort" 23 "strings" 24 "testing" 25 "unicode" 26 27 "go.chromium.org/luci/starlark/builtins" 28 "go.starlark.net/starlark" 29 30 . "github.com/smartystreets/goconvey/convey" 31 ) 32 33 // deindent finds first non-empty and non-whitespace line and subtracts its 34 // indentation from all lines. 35 func deindent(s string) string { 36 lines := strings.Split(s, "\n") 37 38 indent := "" 39 for _, line := range lines { 40 idx := strings.IndexFunc(line, func(r rune) bool { 41 return !unicode.IsSpace(r) 42 }) 43 if idx != -1 { 44 indent = line[:idx] 45 break 46 } 47 } 48 49 if indent == "" { 50 return s 51 } 52 53 trimmed := make([]string, len(lines)) 54 for i, line := range lines { 55 trimmed[i] = strings.TrimPrefix(line, indent) 56 } 57 return strings.Join(trimmed, "\n") 58 } 59 60 // deindentLoader deindents starlark code before returning it. 61 func deindentLoader(files map[string]string) Loader { 62 return func(path string) (_ starlark.StringDict, src string, err error) { 63 body, ok := files[path] 64 if !ok { 65 return nil, "", ErrNoModule 66 } 67 return nil, deindent(body), nil 68 } 69 } 70 71 func TestDeindent(t *testing.T) { 72 t.Parallel() 73 Convey("Works", t, func() { 74 s := deindent(` 75 76 a 77 b 78 c 79 d 80 81 e 82 `) 83 So(s, ShouldResemble, ` 84 85 a 86 b 87 c 88 d 89 90 e 91 `) 92 }) 93 } 94 95 // intrParams are arguments for runIntr helper. 96 type intrParams struct { 97 ctx context.Context 98 99 // scripts contains user-supplied scripts (ones that would normally be loaded 100 // from the file system). If there's main.star script, it will be executed via 101 // LoadModule and its global dict keys returned. 102 scripts map[string]string 103 104 // stdlib contains stdlib source code, as path => body mapping. In particular, 105 // builtins.star will be auto-loaded by the interpreter. 106 stdlib map[string]string 107 108 // package 'custom' is used by tests for Loaders. 109 custom Loader 110 111 predeclared starlark.StringDict 112 preExec func(th *starlark.Thread, module ModuleKey) 113 postExec func(th *starlark.Thread, module ModuleKey) 114 115 visited *[]ModuleKey 116 } 117 118 // runIntr initializes and runs the interpreter over given scripts, by loading 119 // main.star using ExecModule. 120 // 121 // Returns keys of the dict of the main.star script (if any), and a list of 122 // messages logged via print(...). 123 func runIntr(p intrParams) (keys []string, logs []string, err error) { 124 ctx := p.ctx 125 if ctx == nil { 126 ctx = context.Background() 127 } 128 129 intr := Interpreter{ 130 Predeclared: p.predeclared, 131 PreExec: p.preExec, 132 PostExec: p.postExec, 133 Packages: map[string]Loader{ 134 MainPkg: deindentLoader(p.scripts), 135 StdlibPkg: deindentLoader(p.stdlib), 136 "custom": p.custom, 137 }, 138 Logger: func(file string, line int, message string) { 139 logs = append(logs, fmt.Sprintf("[%s:%d] %s", file, line, message)) 140 }, 141 } 142 143 if err = intr.Init(ctx); err != nil { 144 return 145 } 146 147 if _, ok := p.scripts["main.star"]; ok { 148 var dict starlark.StringDict 149 dict, err = intr.ExecModule(ctx, MainPkg, "main.star") 150 if err == nil { 151 keys = make([]string, 0, len(dict)) 152 for k := range dict { 153 keys = append(keys, k) 154 } 155 sort.Strings(keys) 156 } 157 } 158 159 if p.visited != nil { 160 *p.visited = intr.Visited() 161 } 162 163 return 164 } 165 166 // normalizeErr takes an error and extracts a normalized stack trace from it. 167 func normalizeErr(err error) string { 168 if err == nil { 169 return "" 170 } 171 if evalErr, ok := err.(*starlark.EvalError); ok { 172 return builtins.NormalizeStacktrace(evalErr.Backtrace()) 173 } 174 return builtins.NormalizeStacktrace(err.Error()) 175 }