github.com/haraldrudell/parl@v0.4.176/pdebug/stack_test.go (about)

     1  /*
     2  © 2022–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package pdebug
     7  
     8  import (
     9  	"fmt"
    10  	"runtime/debug"
    11  	"strconv"
    12  	"strings"
    13  	"sync"
    14  	"testing"
    15  
    16  	"github.com/haraldrudell/parl"
    17  	"github.com/haraldrudell/parl/perrors"
    18  	"github.com/haraldrudell/parl/pruntime"
    19  	"github.com/haraldrudell/parl/pruntime/pruntimelib"
    20  )
    21  
    22  func TestStack(t *testing.T) {
    23  	var threadID, expStatus = func() (threadID parl.ThreadID, status parl.ThreadStatus) {
    24  		// "goroutine 34 [running]:"
    25  		line := strings.Split(string(debug.Stack()), "\n")[0]
    26  		values := strings.Split(line, "\x20")
    27  		if u64, err := strconv.ParseUint(values[1], 10, 64); err != nil {
    28  			panic(err)
    29  		} else {
    30  			threadID = parl.ThreadID(u64)
    31  		}
    32  		status = parl.ThreadStatus(values[2][1 : len(values[2])-2])
    33  		return
    34  	}()
    35  	var expCreatorPrefix = "testing.(*T)."
    36  	var expShorts0 = fmt.Sprintf("Thread ID: %s", threadID)
    37  
    38  	var expGoFunctionFuncName, expGoFunctionFileLine = func() (funcName, fileLine string) {
    39  		//	- -5: goFunction function
    40  		// - -4: goFunction file-line
    41  		// - -3: cre: function
    42  		// - -2: creator file-line
    43  		// - -1:  last line is empty
    44  		lines := strings.Split(string(debug.Stack()), "\n")
    45  		if len(lines) < 5 {
    46  			panic(perrors.ErrorfPF("too few lines in stack trace: %d need >=5", len(lines)))
    47  		}
    48  		funcLine := lines[len(lines)-5] // "testing.tRunner(0x1400011cb60, 0x1011810a8)"
    49  		packagePath, packageName, typePath, funcName := pruntimelib.SplitAbsoluteFunctionName(funcLine)
    50  		if argIndex := strings.Index(funcName, "("); argIndex != -1 {
    51  			funcName = funcName[:argIndex]
    52  		}
    53  		// "testing.tRunner"
    54  		funcName = packagePath + packageName + "." + typePath + funcName
    55  		// "\t/opt/homebrew/Cellar/go/1.20.4/libexec/src/testing/testing.go:1576 +0x10c"
    56  		fileLine = lines[len(lines)-4]
    57  		if len(fileLine) > 0 {
    58  			fileLine = fileLine[1:]
    59  		}
    60  		if endIndex := strings.Index(fileLine, "\x20"); endIndex != -1 {
    61  			fileLine = fileLine[:endIndex]
    62  		}
    63  		return
    64  	}()
    65  
    66  	var frames []pruntime.Frame
    67  	var actualS string
    68  	var actualSL []string
    69  
    70  	var stack, cL = NewStack(0), pruntime.NewCodeLocation(0)
    71  	if stack == nil {
    72  		t.Fatalf("NewStack nil return")
    73  	}
    74  
    75  	// ID() Status() IsMain() Frames() Creator() Shorts()
    76  	var _ parl.Stack
    77  	if stack.ID() != threadID {
    78  		t.Errorf("stack.ID: %s exp %s", stack.ID(), threadID)
    79  	}
    80  	if stack.Status() != expStatus {
    81  		t.Errorf("stack.Status: %s exp %s", stack.Status(), expStatus)
    82  	}
    83  	if stack.IsMain() {
    84  		t.Errorf("stack.IsMain true: stack:\n%s\n", stack.String())
    85  	}
    86  	frames = stack.Frames()
    87  	if len(frames) < 1 {
    88  		t.Error("stack.Frames empty")
    89  	}
    90  	if frames[0].Loc().Short() != cL.Short() {
    91  		t.Errorf("stack.Frames[0]: %s exp %s", frames[0].Loc().Short(), cL.Short())
    92  	}
    93  	// File: "/opt/homebrew/Cellar/go/1.20.4/libexec/src/testing/testing.go" Line: 1576 FuncName: "testing.tRunner"
    94  	t.Logf(stack.GoFunction().Dump())
    95  	// expGoFunctionFuncName "testing.tRunner"
    96  	t.Logf("expGoFunctionFuncName %q", expGoFunctionFuncName)
    97  	// "/opt/homebrew/Cellar/go/1.20.4/libexec/src/testing/testing.go:1576"
    98  	t.Logf("expGoFunctionFileLine %q", expGoFunctionFileLine)
    99  	if stack.GoFunction().PackFunc() != expGoFunctionFuncName {
   100  		t.Errorf("stack.GoFunction PackFunc: %q expPrefix %q", stack.GoFunction().PackFunc(), expGoFunctionFuncName)
   101  	}
   102  	actualS = stack.GoFunction().File + ":" + strconv.Itoa(stack.GoFunction().Line)
   103  	if actualS != expGoFunctionFileLine {
   104  		t.Errorf("stack.GoFunction FuncLine: %q expPrefix %q", stack.GoFunction().FuncLine(), expGoFunctionFileLine)
   105  	}
   106  	var creator, _, _ = stack.Creator()
   107  	actualS = creator.Short()
   108  	if !strings.HasPrefix(actualS, expCreatorPrefix) {
   109  		t.Errorf("stack.Creator: %q expPrefix %q", actualS, expCreatorPrefix)
   110  	}
   111  	actualSL = strings.Split(stack.Shorts(""), "\n")
   112  	if actualSL[0] != expShorts0 {
   113  		t.Errorf("stack.Shorts 0: %q exp %q", actualSL[0], expShorts0)
   114  	}
   115  	if actualSL[1] != cL.Short() {
   116  		t.Errorf("stack.Shorts 1: %q exp %q", actualSL[1], cL.Short())
   117  	}
   118  }
   119  
   120  func TestStackString(t *testing.T) {
   121  	var creator, goFunction, frame0cL pruntime.CodeLocation
   122  	var stack parl.Stack
   123  	var threadID, parentID parl.ThreadID
   124  	var threadStatus parl.ThreadStatus
   125  
   126  	// populate variables above with predictable values
   127  	var tt = T{
   128  		creator:      &creator,
   129  		goFunction:   &goFunction,
   130  		stack:        &stack,
   131  		frame0:       &frame0cL,
   132  		threadID:     &threadID,
   133  		threadStatus: &threadStatus,
   134  		parentID:     &parentID,
   135  	}
   136  	tt.wg.Add(1)
   137  	go stackGenerator(&tt, pruntime.NewCodeLocation(0))
   138  	tt.wg.Wait()
   139  
   140  	// assemble expected values
   141  
   142  	// The stack is created by T.stack2
   143  	//	- 1 status line: ID: 35 IsMain: false status: running
   144  	//	- 3x2 frames: pdebug.NewStack
   145  	//	- 1 creator line: cre: github.com/haraldrudell/parl/pdebug.TestStackString-stack_test.go:161
   146  	var expLines = 1 + 2*2 + 2 // 6: 0–5: ID, 2x2code locations, cre
   147  	var expLine1 = fmt.Sprintf("ID: %s status: ‘%s’", threadID, threadStatus)
   148  	var expLine3 = "\x20\x20" + frame0cL.File + ":" + strconv.Itoa(frame0cL.Line)
   149  	var expLine5 = "\x20\x20" + goFunction.File + ":" + strconv.Itoa(goFunction.Line)
   150  	var expLine6 = strings.Split(fmt.Sprintf("Parent-ID: %d go: %s", parentID, creator), "\n")[0]
   151  
   152  	var actualSL []string
   153  
   154  	// STACK OBTAINED:
   155  	// ID: 37 status: ‘running’
   156  	// github.com/haraldrudell/parl/pdebug.stack2(0x1400014aa80, 0x14000114cc0, 0x14000114c90)
   157  	// 	/opt/sw/parl/pdebug/stack_test.go:137
   158  	// github.com/haraldrudell/parl/pdebug.stackGenerator(0x14000104ea0?, 0x100abbfd0?)
   159  	// 	/opt/sw/parl/pdebug/stack_test.go:132
   160  	// Parent-ID: 36 go: github.com/haraldrudell/parl/pdebug.TestStackString
   161  	// 	/opt/sw/parl/pdebug/stack_test.go:174
   162  	// —
   163  	t.Logf("STACK OBTAINED:\n%s\n—", stack)
   164  
   165  	actualSL = strings.Split(stack.String(), "\n")
   166  	// length: 6: 0…5
   167  	if len(actualSL) != expLines {
   168  		t.Fatalf("FAIL stack.String lines: %d exp %d", len(actualSL), expLines)
   169  	}
   170  	// ID: 35 IsMain: false status: running
   171  	if actualSL[0] != expLine1 {
   172  		t.Errorf("FAIL stack.String 1: %q exp %q", actualSL[0], expLine1)
   173  	}
   174  	// github.com/haraldrudell/parl/pdebug.stack2(0x1400011ea80, 0x1400010f080, 0x1400010f050)
   175  	if !strings.HasPrefix(actualSL[1], frame0cL.FuncName) {
   176  		t.Errorf("FAIL stack.String frame 0: %q expPrefix %q", actualSL[1], frame0cL.FuncName)
   177  	}
   178  	// stack_test.go:86
   179  	if actualSL[2] != expLine3 {
   180  		t.Errorf("FAIL stack.String frame 1: %q exp %q", actualSL[2], expLine3)
   181  	}
   182  	// github.com/haraldrudell/parl/pdebug.stackGenerator(0x14000003ba0?, 0x102d05020?)
   183  	if !strings.HasPrefix(actualSL[3], goFunction.FuncName) {
   184  		t.Errorf("FAIL stack.String frame 2: %q expPrefix %q", actualSL[3], goFunction.FuncName)
   185  	}
   186  	// stack_test.go:82
   187  	if actualSL[4] != expLine5 {
   188  		t.Errorf("FAIL stack.String frame 3: %q exp %q", actualSL[4], expLine5)
   189  	}
   190  	// cre: github.com/haraldrudell/parl/pdebug.TestStackString-stack_test.go:119
   191  	if !strings.HasPrefix(actualSL[5], expLine6) {
   192  		t.Errorf("FAIL stack.String Creator:\n%q expPrefix\n%q", actualSL[5], expLine6)
   193  	}
   194  }
   195  
   196  func TestParseFirstStackLine(t *testing.T) {
   197  	input := []byte("goroutine 19 [running]:\ngarbage")
   198  	expID := parl.ThreadID(19)
   199  	expStatus := parl.ThreadStatus("running")
   200  
   201  	ID, status, err := ParseFirstLine(input)
   202  	if err != nil {
   203  		t.Errorf("FAIL ParseFirstStackLine err: %v", err)
   204  	}
   205  	if ID != expID {
   206  		t.Errorf("FAIL ID: %q exp: %q", ID, expID)
   207  	}
   208  	if status != expStatus {
   209  		t.Errorf("FAIL status: %q exp: %q", status, expStatus)
   210  	}
   211  }
   212  
   213  // END OF TEST
   214  // END OF TEST
   215  // END OF TEST
   216  
   217  type T struct {
   218  	creator, goFunction, frame0 *pruntime.CodeLocation
   219  	threadID                    *parl.ThreadID
   220  	threadStatus                *parl.ThreadStatus
   221  	stack                       *parl.Stack
   222  	parentID                    *parl.ThreadID
   223  	wg                          sync.WaitGroup
   224  }
   225  
   226  func stackGenerator(t *T, c *pruntime.CodeLocation) { stack2(t, pruntime.NewCodeLocation(0), c) }
   227  
   228  func stack2(t *T, goFunction, creator *pruntime.CodeLocation) {
   229  	defer t.wg.Done()
   230  
   231  	var stack, cL = NewStack(0), pruntime.NewCodeLocation(0)
   232  	*t.creator = *creator
   233  	*t.goFunction = *goFunction
   234  	*t.stack = stack
   235  	*t.frame0 = *cL
   236  	threadID, status := func() (threadID parl.ThreadID, status parl.ThreadStatus) {
   237  		// "goroutine 34 [running]:"
   238  		line := strings.Split(string(debug.Stack()), "\n")[0]
   239  		values := strings.Split(line, "\x20")
   240  		if u64, err := strconv.ParseUint(values[1], 10, 64); err != nil {
   241  			panic(err)
   242  		} else {
   243  			threadID = parl.ThreadID(u64)
   244  		}
   245  		status = parl.ThreadStatus(values[2][1 : len(values[2])-2])
   246  		return
   247  	}()
   248  	*t.threadID = threadID
   249  	*t.threadStatus = status
   250  	_, *t.parentID, _ = stack.Creator()
   251  }
   252  
   253  // single-step through constructor
   254  // func TestStack0(t *testing.T) {
   255  // 	var stack parl.Stack = NewStack(0)
   256  // 	_ = stack
   257  // }