xref: /aosp_15_r20/external/starlark-go/starlarktest/starlarktest.go (revision 4947cdc739c985f6d86941e22894f5cefe7c9e9a)
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