go.starlark.net@v0.0.0-20231101134539-556fd59b42f6/starlarktest/starlarktest.go (about) 1 // Copyright 2017 The Bazel Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package starlarktest defines utilities for testing Starlark programs. 6 // 7 // Clients can call LoadAssertModule to load a module that defines 8 // several functions useful for testing. See assert.star for its 9 // definition. 10 // 11 // The assert.error function, which reports errors to the current Go 12 // testing.T, requires that clients call SetReporter(thread, t) before use. 13 package starlarktest // import "go.starlark.net/starlarktest" 14 15 import ( 16 _ "embed" 17 "fmt" 18 "math" 19 "os" 20 "path/filepath" 21 "regexp" 22 "strings" 23 "sync" 24 25 "go.starlark.net/starlark" 26 "go.starlark.net/starlarkstruct" 27 ) 28 29 const localKey = "Reporter" 30 31 // A Reporter is a value to which errors may be reported. 32 // It is satisfied by *testing.T. 33 type Reporter interface { 34 Error(args ...interface{}) 35 } 36 37 // SetReporter associates an error reporter (such as a testing.T in 38 // a Go test) with the Starlark thread so that Starlark programs may 39 // report errors to it. 40 func SetReporter(thread *starlark.Thread, r Reporter) { 41 thread.SetLocal(localKey, r) 42 } 43 44 // GetReporter returns the Starlark thread's error reporter. 45 // It must be preceded by a call to SetReporter. 46 func GetReporter(thread *starlark.Thread) Reporter { 47 r, ok := thread.Local(localKey).(Reporter) 48 if !ok { 49 panic("internal error: starlarktest.SetReporter was not called") 50 } 51 return r 52 } 53 54 var ( 55 once sync.Once 56 assert starlark.StringDict 57 //go:embed assert.star 58 assertFileSrc string 59 assertErr error 60 ) 61 62 // LoadAssertModule loads the assert module. 63 // It is concurrency-safe and idempotent. 64 func LoadAssertModule() (starlark.StringDict, error) { 65 once.Do(func() { 66 predeclared := starlark.StringDict{ 67 "error": starlark.NewBuiltin("error", error_), 68 "catch": starlark.NewBuiltin("catch", catch), 69 "matches": starlark.NewBuiltin("matches", matches), 70 "module": starlark.NewBuiltin("module", starlarkstruct.MakeModule), 71 "_freeze": starlark.NewBuiltin("freeze", freeze), 72 "_floateq": starlark.NewBuiltin("floateq", floateq), 73 } 74 thread := new(starlark.Thread) 75 assert, assertErr = starlark.ExecFile(thread, "assert.star", assertFileSrc, predeclared) 76 }) 77 return assert, assertErr 78 } 79 80 // catch(f) evaluates f() and returns its evaluation error message 81 // if it failed or None if it succeeded. 82 func catch(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 83 var fn starlark.Callable 84 if err := starlark.UnpackArgs("catch", args, kwargs, "fn", &fn); err != nil { 85 return nil, err 86 } 87 if _, err := starlark.Call(thread, fn, nil, nil); err != nil { 88 return starlark.String(err.Error()), nil 89 } 90 return starlark.None, nil 91 } 92 93 // matches(pattern, str) reports whether string str matches the regular expression pattern. 94 func matches(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 95 var pattern, str string 96 if err := starlark.UnpackArgs("matches", args, kwargs, "pattern", &pattern, "str", &str); err != nil { 97 return nil, err 98 } 99 ok, err := regexp.MatchString(pattern, str) 100 if err != nil { 101 return nil, fmt.Errorf("matches: %s", err) 102 } 103 return starlark.Bool(ok), nil 104 } 105 106 // error(x) reports an error to the Go test framework. 107 func error_(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 108 if len(args) != 1 { 109 return nil, fmt.Errorf("error: got %d arguments, want 1", len(args)) 110 } 111 buf := new(strings.Builder) 112 stk := thread.CallStack() 113 stk.Pop() 114 fmt.Fprintf(buf, "%sError: ", stk) 115 if s, ok := starlark.AsString(args[0]); ok { 116 buf.WriteString(s) 117 } else { 118 buf.WriteString(args[0].String()) 119 } 120 GetReporter(thread).Error(buf.String()) 121 return starlark.None, nil 122 } 123 124 // freeze(x) freezes its operand. 125 func freeze(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 126 if len(kwargs) > 0 { 127 return nil, fmt.Errorf("freeze does not accept keyword arguments") 128 } 129 if len(args) != 1 { 130 return nil, fmt.Errorf("freeze got %d arguments, wants 1", len(args)) 131 } 132 args[0].Freeze() 133 return args[0], nil 134 } 135 136 // floateq(x, y) reports whether two floats are within 1 ULP of each other. 137 func floateq(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 138 var xf, yf starlark.Float 139 if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 2, &xf, &yf); err != nil { 140 return nil, err 141 } 142 143 res := false 144 switch { 145 case xf == yf: 146 res = true 147 case math.IsNaN(float64(xf)): 148 res = math.IsNaN(float64(yf)) 149 case math.IsNaN(float64(yf)): 150 // false (non-NaN = Nan) 151 default: 152 x := math.Float64bits(float64(xf)) 153 y := math.Float64bits(float64(yf)) 154 res = x == y+1 || y == x+1 155 } 156 return starlark.Bool(res), nil 157 } 158 159 // DataFile returns the effective filename of the specified 160 // test data resource. The function abstracts differences between 161 // 'go build', under which a test runs in its package directory, 162 // and Blaze, under which a test runs in the root of the tree. 163 var DataFile = func(pkgdir, filename string) string { 164 // Check if we're being run by Bazel and change directories if so. 165 // TEST_SRCDIR and TEST_WORKSPACE are set by the Bazel test runner, so that makes a decent check 166 testSrcdir := os.Getenv("TEST_SRCDIR") 167 testWorkspace := os.Getenv("TEST_WORKSPACE") 168 if testSrcdir != "" && testWorkspace != "" { 169 return filepath.Join(testSrcdir, "net_starlark_go", pkgdir, filename) 170 } 171 172 // Under go test, ignore pkgdir, which is the directory of the 173 // current package relative to the module root. 174 return filename 175 }