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  }