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. 13package starlarktest // import "go.starlark.net/starlarktest" 14 15import ( 16 "fmt" 17 "go/build" 18 "os" 19 "path/filepath" 20 "regexp" 21 "strings" 22 "sync" 23 24 "go.starlark.net/starlark" 25 "go.starlark.net/starlarkstruct" 26) 27 28const localKey = "Reporter" 29 30// A Reporter is a value to which errors may be reported. 31// It is satisfied by *testing.T. 32type Reporter interface { 33 Error(args ...interface{}) 34} 35 36// SetReporter associates an error reporter (such as a testing.T in 37// a Go test) with the Starlark thread so that Starlark programs may 38// report errors to it. 39func SetReporter(thread *starlark.Thread, r Reporter) { 40 thread.SetLocal(localKey, r) 41} 42 43// GetReporter returns the Starlark thread's error reporter. 44// It must be preceded by a call to SetReporter. 45func GetReporter(thread *starlark.Thread) Reporter { 46 r, ok := thread.Local(localKey).(Reporter) 47 if !ok { 48 panic("internal error: starlarktest.SetReporter was not called") 49 } 50 return r 51} 52 53var ( 54 once sync.Once 55 assert starlark.StringDict 56 assertErr error 57) 58 59// LoadAssertModule loads the assert module. 60// It is concurrency-safe and idempotent. 61func LoadAssertModule() (starlark.StringDict, error) { 62 once.Do(func() { 63 predeclared := starlark.StringDict{ 64 "error": starlark.NewBuiltin("error", error_), 65 "catch": starlark.NewBuiltin("catch", catch), 66 "matches": starlark.NewBuiltin("matches", matches), 67 "module": starlark.NewBuiltin("module", starlarkstruct.MakeModule), 68 "_freeze": starlark.NewBuiltin("freeze", freeze), 69 } 70 filename := DataFile("starlarktest", "assert.star") 71 thread := new(starlark.Thread) 72 assert, assertErr = starlark.ExecFile(thread, filename, nil, predeclared) 73 }) 74 return assert, assertErr 75} 76 77// catch(f) evaluates f() and returns its evaluation error message 78// if it failed or None if it succeeded. 79func catch(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 80 var fn starlark.Callable 81 if err := starlark.UnpackArgs("catch", args, kwargs, "fn", &fn); err != nil { 82 return nil, err 83 } 84 if _, err := starlark.Call(thread, fn, nil, nil); err != nil { 85 return starlark.String(err.Error()), nil 86 } 87 return starlark.None, nil 88} 89 90// matches(pattern, str) reports whether string str matches the regular expression pattern. 91func matches(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 92 var pattern, str string 93 if err := starlark.UnpackArgs("matches", args, kwargs, "pattern", &pattern, "str", &str); err != nil { 94 return nil, err 95 } 96 ok, err := regexp.MatchString(pattern, str) 97 if err != nil { 98 return nil, fmt.Errorf("matches: %s", err) 99 } 100 return starlark.Bool(ok), nil 101} 102 103// error(x) reports an error to the Go test framework. 104func error_(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 105 if len(args) != 1 { 106 return nil, fmt.Errorf("error: got %d arguments, want 1", len(args)) 107 } 108 buf := new(strings.Builder) 109 stk := thread.CallStack() 110 stk.Pop() 111 fmt.Fprintf(buf, "%sError: ", stk) 112 if s, ok := starlark.AsString(args[0]); ok { 113 buf.WriteString(s) 114 } else { 115 buf.WriteString(args[0].String()) 116 } 117 GetReporter(thread).Error(buf.String()) 118 return starlark.None, nil 119} 120 121// freeze(x) freezes its operand. 122func freeze(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 123 if len(kwargs) > 0 { 124 return nil, fmt.Errorf("freeze does not accept keyword arguments") 125 } 126 if len(args) != 1 { 127 return nil, fmt.Errorf("freeze got %d arguments, wants 1", len(args)) 128 } 129 args[0].Freeze() 130 return args[0], nil 131} 132 133// DataFile returns the effective filename of the specified 134// test data resource. The function abstracts differences between 135// 'go build', under which a test runs in its package directory, 136// and Blaze, under which a test runs in the root of the tree. 137var DataFile = func(pkgdir, filename string) string { 138 // Check if we're being run by Bazel and change directories if so. 139 // TEST_SRCDIR and TEST_WORKSPACE are set by the Bazel test runner, so that makes a decent check 140 testSrcdir := os.Getenv("TEST_SRCDIR") 141 testWorkspace := os.Getenv("TEST_WORKSPACE") 142 if testSrcdir != "" && testWorkspace != "" { 143 return filepath.Join(testSrcdir, "net_starlark_go", pkgdir, filename) 144 } 145 146 return filepath.Join(build.Default.GOPATH, "src/go.starlark.net", pkgdir, filename) 147} 148