go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/starlark/builtins/fail.go (about)

     1  // Copyright 2018 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package builtins
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"strings"
    21  
    22  	"go.starlark.net/starlark"
    23  )
    24  
    25  // Failure is an error emitted by fail(...) and captured by FailureCollector.
    26  type Failure struct {
    27  	Message   string              // the error message, as passed to fail(...)
    28  	UserTrace *CapturedStacktrace // value of 'trace' passed to fail or nil
    29  	FailTrace *CapturedStacktrace // where 'fail' itself was called
    30  }
    31  
    32  // Error is the short error message, as passed to fail(...).
    33  func (f *Failure) Error() string {
    34  	return f.Message
    35  }
    36  
    37  // Backtrace returns a user-friendly error message describing the stack of
    38  // calls that led to this error.
    39  //
    40  // If fail(...) was called with a custom stack trace, this trace is shown here.
    41  // Otherwise the trace of where fail(...) happened is used.
    42  func (f *Failure) Backtrace() string {
    43  	tr := f.UserTrace
    44  	if tr == nil {
    45  		tr = f.FailTrace
    46  	}
    47  	return tr.String() + "Error: " + f.Message
    48  }
    49  
    50  // Fail is fail(*args, sep=" ", trace=None) builtin.
    51  //
    52  //	def fail(*args, sep=" ", trace=None):
    53  //	  """Aborts the script execution with an error message."
    54  //
    55  //	  Args:
    56  //	    args: values to print in the message.
    57  //	    sep: separator to use between values from `args`.
    58  //	    trace: a trace (as returned by stacktrace()) to attach to the error.
    59  //	  """
    60  //
    61  // Custom stack traces are recoverable through FailureCollector. This is due
    62  // to Starlark's insistence on stringying all errors. If there's no
    63  // FailureCollector in the thread locals, custom traces are silently ignored.
    64  //
    65  // Note that the assert.fails(...) check in the default starlark tests library
    66  // doesn't clear the failure collector state when it "catches" an error, so
    67  // tests that use assert.fails(...) should be careful with using the failure
    68  // collector (or just don't use it at all).
    69  var Fail = starlark.NewBuiltin("fail", func(th *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
    70  	sep := " "
    71  	var trace starlark.Value
    72  	err := starlark.UnpackArgs("fail", nil, kwargs,
    73  		"sep?", &sep,
    74  		"trace?", &trace)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	var userTrace *CapturedStacktrace
    80  	if trace != nil && trace != starlark.None {
    81  		if userTrace, _ = trace.(*CapturedStacktrace); userTrace == nil {
    82  			return nil, fmt.Errorf("fail: bad 'trace' - got %s, expecting stacktrace", trace.Type())
    83  		}
    84  	}
    85  
    86  	buf := strings.Builder{}
    87  	for i, v := range args {
    88  		if i > 0 {
    89  			buf.WriteString(sep)
    90  		}
    91  		if s, ok := starlark.AsString(v); ok {
    92  			buf.WriteString(s)
    93  		} else {
    94  			buf.WriteString(v.String())
    95  		}
    96  	}
    97  	msg := buf.String()
    98  
    99  	if fc := GetFailureCollector(th); fc != nil {
   100  		failTrace, _ := CaptureStacktrace(th, 0)
   101  		fc.failure = &Failure{
   102  			Message:   msg,
   103  			UserTrace: userTrace,
   104  			FailTrace: failTrace,
   105  		}
   106  	}
   107  
   108  	return nil, errors.New(msg)
   109  })
   110  
   111  // A key in thread.Locals to hold *FailureCollector.
   112  const failSlotKey = "go.chromium.org/luci/starlark/builtins.FailureCollector"
   113  
   114  // FailureCollector receives structured error messages from fail(...).
   115  //
   116  // It should be installed into Starlark thread locals (via Install) for
   117  // fail(...) to be able to discover it. If it's not there, fail(...) will not
   118  // return any additional information (like a custom stack trace) besides the
   119  // information contained in *starlark.EvalError.
   120  type FailureCollector struct {
   121  	// failure is the error passed to fail(...).
   122  	//
   123  	// fail(...) aborts the execution of starlark scripts, so its fine to keep
   124  	// only one error. There can't really be more.
   125  	failure *Failure
   126  }
   127  
   128  // GetFailureCollector returns a failure collector installed in the thread.
   129  func GetFailureCollector(th *starlark.Thread) *FailureCollector {
   130  	fc, _ := th.Local(failSlotKey).(*FailureCollector)
   131  	return fc
   132  }
   133  
   134  // Install installs this failure collector into the thread.
   135  func (fc *FailureCollector) Install(t *starlark.Thread) {
   136  	t.SetLocal(failSlotKey, fc)
   137  }
   138  
   139  // LatestFailure returns the latest captured failure or nil if there are none.
   140  func (fc *FailureCollector) LatestFailure() *Failure {
   141  	return fc.failure
   142  }
   143  
   144  // Clear resets the state.
   145  //
   146  // Useful if the same FailureCollector is reused between calls to Starlark.
   147  func (fc *FailureCollector) Clear() {
   148  	fc.failure = nil
   149  }