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 }