// Copyright 2017 The Bazel Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package starlarktest defines utilities for testing Starlark programs. // // Clients can call LoadAssertModule to load a module that defines // several functions useful for testing. See assert.star for its // definition. // // The assert.error function, which reports errors to the current Go // testing.T, requires that clients call SetReporter(thread, t) before use. package starlarktest // import "go.starlark.net/starlarktest" import ( "fmt" "go/build" "os" "path/filepath" "regexp" "strings" "sync" "go.starlark.net/starlark" "go.starlark.net/starlarkstruct" ) const localKey = "Reporter" // A Reporter is a value to which errors may be reported. // It is satisfied by *testing.T. type Reporter interface { Error(args ...interface{}) } // SetReporter associates an error reporter (such as a testing.T in // a Go test) with the Starlark thread so that Starlark programs may // report errors to it. func SetReporter(thread *starlark.Thread, r Reporter) { thread.SetLocal(localKey, r) } // GetReporter returns the Starlark thread's error reporter. // It must be preceded by a call to SetReporter. func GetReporter(thread *starlark.Thread) Reporter { r, ok := thread.Local(localKey).(Reporter) if !ok { panic("internal error: starlarktest.SetReporter was not called") } return r } var ( once sync.Once assert starlark.StringDict assertErr error ) // LoadAssertModule loads the assert module. // It is concurrency-safe and idempotent. func LoadAssertModule() (starlark.StringDict, error) { once.Do(func() { predeclared := starlark.StringDict{ "error": starlark.NewBuiltin("error", error_), "catch": starlark.NewBuiltin("catch", catch), "matches": starlark.NewBuiltin("matches", matches), "module": starlark.NewBuiltin("module", starlarkstruct.MakeModule), "_freeze": starlark.NewBuiltin("freeze", freeze), } filename := DataFile("starlarktest", "assert.star") thread := new(starlark.Thread) assert, assertErr = starlark.ExecFile(thread, filename, nil, predeclared) }) return assert, assertErr } // catch(f) evaluates f() and returns its evaluation error message // if it failed or None if it succeeded. func catch(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { var fn starlark.Callable if err := starlark.UnpackArgs("catch", args, kwargs, "fn", &fn); err != nil { return nil, err } if _, err := starlark.Call(thread, fn, nil, nil); err != nil { return starlark.String(err.Error()), nil } return starlark.None, nil } // matches(pattern, str) reports whether string str matches the regular expression pattern. func matches(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { var pattern, str string if err := starlark.UnpackArgs("matches", args, kwargs, "pattern", &pattern, "str", &str); err != nil { return nil, err } ok, err := regexp.MatchString(pattern, str) if err != nil { return nil, fmt.Errorf("matches: %s", err) } return starlark.Bool(ok), nil } // error(x) reports an error to the Go test framework. func error_(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { if len(args) != 1 { return nil, fmt.Errorf("error: got %d arguments, want 1", len(args)) } buf := new(strings.Builder) stk := thread.CallStack() stk.Pop() fmt.Fprintf(buf, "%sError: ", stk) if s, ok := starlark.AsString(args[0]); ok { buf.WriteString(s) } else { buf.WriteString(args[0].String()) } GetReporter(thread).Error(buf.String()) return starlark.None, nil } // freeze(x) freezes its operand. func freeze(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { if len(kwargs) > 0 { return nil, fmt.Errorf("freeze does not accept keyword arguments") } if len(args) != 1 { return nil, fmt.Errorf("freeze got %d arguments, wants 1", len(args)) } args[0].Freeze() return args[0], nil } // DataFile returns the effective filename of the specified // test data resource. The function abstracts differences between // 'go build', under which a test runs in its package directory, // and Blaze, under which a test runs in the root of the tree. var DataFile = func(pkgdir, filename string) string { // Check if we're being run by Bazel and change directories if so. // TEST_SRCDIR and TEST_WORKSPACE are set by the Bazel test runner, so that makes a decent check testSrcdir := os.Getenv("TEST_SRCDIR") testWorkspace := os.Getenv("TEST_WORKSPACE") if testSrcdir != "" && testWorkspace != "" { return filepath.Join(testSrcdir, "net_starlark_go", pkgdir, filename) } return filepath.Join(build.Default.GOPATH, "src/go.starlark.net", pkgdir, filename) }