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  }