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 // }