go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/starlark/builtins/stacktrace.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  	"regexp"
    21  	"strings"
    22  
    23  	"go.starlark.net/starlark"
    24  )
    25  
    26  // CapturedStacktrace represents a stack trace returned by stacktrace(...).
    27  //
    28  // At the present time it can only be stringified (via str(...) in Starlark or
    29  // via .String() in Go).
    30  type CapturedStacktrace struct {
    31  	backtrace string
    32  }
    33  
    34  // CaptureStacktrace captures thread's stack trace, skipping some number of
    35  // innermost frames.
    36  //
    37  // Returns an error if the stack is not deep enough to skip the requested number
    38  // of frames.
    39  func CaptureStacktrace(th *starlark.Thread, skip int) (*CapturedStacktrace, error) {
    40  	if th.CallStackDepth() <= skip {
    41  		return nil, fmt.Errorf("stacktrace: the stack is not deep enough to skip %d levels, has only %d frames", skip, th.CallStackDepth())
    42  	}
    43  
    44  	stack := th.CallStack()[:th.CallStackDepth()-skip]
    45  	return &CapturedStacktrace{stack.String()}, nil
    46  }
    47  
    48  // Type is part of starlark.Value interface.
    49  func (*CapturedStacktrace) Type() string { return "stacktrace" }
    50  
    51  // Freeze is part of starlark.Value interface.
    52  func (*CapturedStacktrace) Freeze() {}
    53  
    54  // Truth is part of starlark.Value interface.
    55  func (*CapturedStacktrace) Truth() starlark.Bool { return starlark.True }
    56  
    57  // Hash is part of starlark.Value interface.
    58  func (*CapturedStacktrace) Hash() (uint32, error) { return 0, errors.New("stacktrace is unhashable") }
    59  
    60  // String is part of starlark.Value interface.
    61  //
    62  // Renders the stack trace as string.
    63  func (s *CapturedStacktrace) String() string { return s.backtrace }
    64  
    65  // Stacktrace is stacktrace(...) builtin.
    66  //
    67  //	def stacktrace(skip=0):
    68  //	  """Capture and returns a stack trace of the caller.
    69  //
    70  //	  A captured stacktrace is an opaque object that can be stringified to get a
    71  //	  nice looking trace (e.g. for error messages).
    72  //
    73  //	  Args:
    74  //	    skip: how many inner most frames to skip.
    75  //	  """
    76  var Stacktrace = starlark.NewBuiltin("stacktrace", func(th *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
    77  	skip := starlark.MakeInt(0)
    78  	if err := starlark.UnpackArgs("stacktrace", args, kwargs, "skip?", &skip); err != nil {
    79  		return nil, err
    80  	}
    81  	switch lvl, err := starlark.AsInt32(skip); {
    82  	case err != nil:
    83  		return nil, fmt.Errorf("stacktrace: bad 'skip' value %s - %s", skip, err)
    84  	case lvl < 0:
    85  		return nil, fmt.Errorf("stacktrace: bad 'skip' value %d - must be non-negative", lvl)
    86  	default:
    87  		return CaptureStacktrace(th, lvl)
    88  	}
    89  })
    90  
    91  // This matches "<module>:<line>:<col>: in <func>" where <line>:<col> is
    92  // optional.
    93  var stackLineRe = regexp.MustCompile(`^(\s*)(.*?): in (.*)$`)
    94  
    95  // NormalizeStacktrace removes mentions of line and column numbers from a
    96  // rendered stack trace: "main:1:5: in <toplevel>" => "main: in <toplevel>".
    97  //
    98  // Useful when comparing stack traces in tests to make the comparison less
    99  // brittle.
   100  func NormalizeStacktrace(trace string) string {
   101  	lines := strings.Split(trace, "\n")
   102  	for i := range lines {
   103  		if m := stackLineRe.FindStringSubmatch(lines[i]); m != nil {
   104  			space, module, funcname := m[1], m[2], m[3]
   105  			if idx := strings.IndexRune(module, ':'); idx != -1 {
   106  				module = module[:idx]
   107  			}
   108  			lines[i] = fmt.Sprintf("%s%s: in %s", space, module, funcname)
   109  		}
   110  	}
   111  	return strings.Join(lines, "\n")
   112  }