diff --git a/api_routine.go b/api_routine.go index f8a7e03..bc9aa55 100644 --- a/api_routine.go +++ b/api_routine.go @@ -25,9 +25,10 @@ func WrapTask(fun Runnable) FutureTask { // catch defer func() { if cause := recover(); cause != nil { - err := NewRuntimeError(cause) - task.Fail(err) - fmt.Println(err.Error()) + task.Fail(cause) + if err := task.(*futureTask).error; err != nil { + fmt.Println(err.Error()) + } } }() // restore @@ -70,7 +71,7 @@ func WrapWaitTask(fun CancelRunnable) FutureTask { // catch defer func() { if cause := recover(); cause != nil { - task.Fail(NewRuntimeError(cause)) + task.Fail(cause) } }() // restore @@ -113,7 +114,7 @@ func WrapWaitResultTask(fun CancelCallable) FutureTask { // catch defer func() { if cause := recover(); cause != nil { - task.Fail(NewRuntimeError(cause)) + task.Fail(cause) } }() // restore diff --git a/error.go b/error.go index f0c6639..995a8c7 100644 --- a/error.go +++ b/error.go @@ -1,11 +1,11 @@ package routine import ( + "bytes" "fmt" "reflect" "runtime" "strconv" - "strings" "unicode" ) @@ -89,13 +89,13 @@ func runtimeErrorNewWithMessageCause(message string, cause any) (goid int64, gop } func runtimeErrorError(re RuntimeError) string { - builder := &strings.Builder{} + builder := &bytes.Buffer{} runtimeErrorPrintStackTrace(re, builder) runtimeErrorPrintCreatedBy(re, builder) return builder.String() } -func runtimeErrorPrintStackTrace(re RuntimeError, builder *strings.Builder) { +func runtimeErrorPrintStackTrace(re RuntimeError, builder *bytes.Buffer) { builder.WriteString(runtimeErrorTypeName(re)) message := re.Message() if len(message) > 0 { @@ -113,10 +113,12 @@ func runtimeErrorPrintStackTrace(re RuntimeError, builder *strings.Builder) { } stackTrace := re.StackTrace() if stackTrace != nil { + savePoint := builder.Len() + skippedPanic := false frames := runtime.CallersFrames(stackTrace) for { frame, more := frames.Next() - if len(frame.Function) > 0 && frame.Function != "runtime.goexit" { + if showFrame(frame.Function) { builder.WriteString(newLine) builder.WriteString(" ") builder.WriteString(wordAt) @@ -128,6 +130,9 @@ func runtimeErrorPrintStackTrace(re RuntimeError, builder *strings.Builder) { builder.WriteString(frame.File) builder.WriteString(":") builder.WriteString(strconv.Itoa(frame.Line)) + } else if skipFrame(frame.Function, skippedPanic) { + builder.Truncate(savePoint) + skippedPanic = true } if !more { break @@ -136,7 +141,7 @@ func runtimeErrorPrintStackTrace(re RuntimeError, builder *strings.Builder) { } } -func runtimeErrorPrintCreatedBy(re RuntimeError, builder *strings.Builder) { +func runtimeErrorPrintCreatedBy(re RuntimeError, builder *bytes.Buffer) { goid := re.Goid() if goid == 1 { return diff --git a/error_test.go b/error_test.go index 768a3b2..a0094b6 100644 --- a/error_test.go +++ b/error_test.go @@ -50,6 +50,33 @@ func TestRuntimeError_StackTrace(t *testing.T) { } } +func TestRuntimeError_Panic_Panic(t *testing.T) { + defer func() { + cause := recover() + assert.NotNil(t, cause) + err := NewRuntimeError(cause) + lines := strings.Split(err.Error(), newLine) + assert.Equal(t, 6, len(lines)) + // + line := lines[0] + assert.Equal(t, "RuntimeError: 1", line) + // + line = lines[1] + assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestRuntimeError_Panic_Panic.")) + assert.True(t, strings.HasSuffix(line, "error_test.go:74")) + // + line = lines[2] + assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestRuntimeError_Panic_Panic()")) + assert.True(t, strings.HasSuffix(line, "error_test.go:77")) + }() + defer func() { + if cause := recover(); cause != nil { + panic(cause) + } + }() + panic(1) +} + func TestRuntimeError_Cause(t *testing.T) { err := NewRuntimeError(nil) assert.Nil(t, err.Cause()) @@ -74,7 +101,7 @@ func TestRuntimeError_Error_EmptyMessage_NilError(t *testing.T) { // line = lines[1] assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestRuntimeError_Error_EmptyMessage_NilError() in ")) - assert.True(t, strings.HasSuffix(line, "error_test.go:68")) + assert.True(t, strings.HasSuffix(line, "error_test.go:95")) // line = lines[2] assert.True(t, strings.HasPrefix(line, " at testing.tRunner() in ")) @@ -100,7 +127,7 @@ func TestRuntimeError_Error_EmptyMessage_NormalError(t *testing.T) { // line = lines[2] assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestRuntimeError_Error_EmptyMessage_NormalError() in ")) - assert.True(t, strings.HasSuffix(line, "error_test.go:90")) + assert.True(t, strings.HasSuffix(line, "error_test.go:117")) // line = lines[3] assert.True(t, strings.HasPrefix(line, " at testing.tRunner() in ")) @@ -110,7 +137,7 @@ func TestRuntimeError_Error_EmptyMessage_NormalError(t *testing.T) { // line = lines[5] assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestRuntimeError_Error_EmptyMessage_NormalError() in ")) - assert.True(t, strings.HasSuffix(line, "error_test.go:91")) + assert.True(t, strings.HasSuffix(line, "error_test.go:118")) // line = lines[6] assert.True(t, strings.HasPrefix(line, " at testing.tRunner() in ")) @@ -132,7 +159,7 @@ func TestRuntimeError_Error_NormalMessage_NilError(t *testing.T) { // line = lines[1] assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestRuntimeError_Error_NormalMessage_NilError() in ")) - assert.True(t, strings.HasSuffix(line, "error_test.go:126")) + assert.True(t, strings.HasSuffix(line, "error_test.go:153")) // line = lines[2] assert.True(t, strings.HasPrefix(line, " at testing.tRunner() in ")) @@ -158,7 +185,7 @@ func TestRuntimeError_Error_NormalMessage_NormalError(t *testing.T) { // line = lines[2] assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestRuntimeError_Error_NormalMessage_NormalError() in ")) - assert.True(t, strings.HasSuffix(line, "error_test.go:148")) + assert.True(t, strings.HasSuffix(line, "error_test.go:175")) // line = lines[3] assert.True(t, strings.HasPrefix(line, " at testing.tRunner() in ")) @@ -168,7 +195,7 @@ func TestRuntimeError_Error_NormalMessage_NormalError(t *testing.T) { // line = lines[5] assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestRuntimeError_Error_NormalMessage_NormalError() in ")) - assert.True(t, strings.HasSuffix(line, "error_test.go:149")) + assert.True(t, strings.HasSuffix(line, "error_test.go:176")) // line = lines[6] assert.True(t, strings.HasPrefix(line, " at testing.tRunner() in ")) @@ -215,7 +242,7 @@ func TestRuntimeError_Error_MainGoid(t *testing.T) { // line = lines[1] assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestRuntimeError_Error_MainGoid() in ")) - assert.True(t, strings.HasSuffix(line, "error_test.go:208")) + assert.True(t, strings.HasSuffix(line, "error_test.go:235")) // line = lines[2] assert.True(t, strings.HasPrefix(line, " at testing.tRunner() in ")) @@ -232,7 +259,7 @@ func TestRuntimeError_Error_ZeroGopc(t *testing.T) { // line = lines[1] assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestRuntimeError_Error_ZeroGopc() in ")) - assert.True(t, strings.HasSuffix(line, "error_test.go:225")) + assert.True(t, strings.HasSuffix(line, "error_test.go:252")) // line = lines[2] assert.True(t, strings.HasPrefix(line, " at testing.tRunner() in ")) @@ -283,6 +310,37 @@ func TestArgumentNilError_StackTrace(t *testing.T) { } } +func TestArgumentNilError_Panic_Panic(t *testing.T) { + defer func() { + cause := recover() + assert.NotNil(t, cause) + err := NewArgumentNilError("a", nil) + lines := strings.Split(err.Error(), newLine) + assert.Equal(t, 7, len(lines)) + // + line := lines[0] + assert.Equal(t, "ArgumentNilError: Value cannot be null.", line) + // + line = lines[1] + assert.Equal(t, "Parameter name: a.", line) + // + line = lines[2] + assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestArgumentNilError_Panic_Panic.")) + assert.True(t, strings.HasSuffix(line, "error_test.go:337")) + // + line = lines[3] + assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestArgumentNilError_Panic_Panic()")) + assert.True(t, strings.HasSuffix(line, "error_test.go:341")) + }() + defer func() { + if cause := recover(); cause != nil { + panic(cause) + } + }() + var a any + _ = a.(string) +} + func TestArgumentNilError_Cause(t *testing.T) { err := NewArgumentNilError("", nil) assert.Nil(t, err.Cause()) @@ -314,7 +372,7 @@ func TestArgumentNilError_Error(t *testing.T) { // line = lines[3] assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestArgumentNilError_Error() in ")) - assert.True(t, strings.HasSuffix(line, "error_test.go:301")) + assert.True(t, strings.HasSuffix(line, "error_test.go:359")) // line = lines[4] assert.True(t, strings.HasPrefix(line, " at testing.tRunner() in ")) @@ -324,7 +382,7 @@ func TestArgumentNilError_Error(t *testing.T) { // line = lines[6] assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestArgumentNilError_Error() in ")) - assert.True(t, strings.HasSuffix(line, "error_test.go:302")) + assert.True(t, strings.HasSuffix(line, "error_test.go:360")) // line = lines[7] assert.True(t, strings.HasPrefix(line, " at testing.tRunner() in ")) diff --git a/future_task.go b/future_task.go index a9f4b9f..885e22a 100644 --- a/future_task.go +++ b/future_task.go @@ -102,7 +102,7 @@ func (task *futureTask) Run() { if atomic.CompareAndSwapInt32(&task.state, taskStateNew, taskStateRunning) { defer func() { if cause := recover(); cause != nil { - task.Fail(NewRuntimeError(cause)) + task.Fail(cause) } }() result := task.callable(task) diff --git a/future_task_test.go b/future_task_test.go index bd5a969..9329dfc 100644 --- a/future_task_test.go +++ b/future_task_test.go @@ -154,19 +154,8 @@ func TestFutureTask_Fail_Common(t *testing.T) { assert.Equal(t, "RuntimeError: 1", line) // line = lines[1] - assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.(*futureTask).Fail() in ")) - assert.True(t, strings.HasSuffix(line, "future_task.go:62")) - // - line = lines[2] assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestFutureTask_Fail_Common.")) - assert.True(t, strings.HasSuffix(line, "future_task_test.go:177")) - // - line = lines[3] - assert.True(t, strings.HasPrefix(line, " at runtime.gopanic() in ")) - // - line = lines[4] - assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestFutureTask_Fail_Common.")) - assert.True(t, strings.HasSuffix(line, "future_task_test.go:180")) + assert.True(t, strings.HasSuffix(line, "future_task_test.go:169")) } }() // @@ -190,20 +179,21 @@ func TestFutureTask_Fail_RuntimeError(t *testing.T) { assert.NotNil(t, err) assert.Equal(t, "1", err.Message()) lines := strings.Split(err.Error(), newLine) + assert.Equal(t, 4, len(lines)) // line := lines[0] assert.Equal(t, "RuntimeError: 1", line) // line = lines[1] assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestFutureTask_Fail_RuntimeError.")) - assert.True(t, strings.HasSuffix(line, "future_task_test.go:214")) + assert.True(t, strings.HasSuffix(line, "future_task_test.go:207")) // line = lines[2] - assert.True(t, strings.HasPrefix(line, " at runtime.gopanic() in ")) + assert.Equal(t, " --- End of error stack trace ---", line) // line = lines[3] - assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestFutureTask_Fail_RuntimeError.")) - assert.True(t, strings.HasSuffix(line, "future_task_test.go:217")) + assert.True(t, strings.HasPrefix(line, " created by github.com/timandy/routine.TestFutureTask_Fail_RuntimeError()")) + assert.True(t, strings.HasSuffix(line, "future_task_test.go:201")) } }() // @@ -420,14 +410,25 @@ func TestFutureTask_Run_Error(t *testing.T) { err := cause.(RuntimeError) assert.Equal(t, "1", err.Message()) lines := strings.Split(err.Error(), newLine) - assert.Equal(t, 7, len(lines)) + assert.Equal(t, 5, len(lines)) // line := lines[0] assert.Equal(t, "RuntimeError: 1", line) // line = lines[1] - assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.(*futureTask).Run.")) - assert.True(t, strings.HasSuffix(line, "future_task.go:105")) + assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestFutureTask_Run_Error.")) + assert.True(t, strings.HasSuffix(line, "future_task_test.go:397")) + // + line = lines[2] + assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.(*futureTask).Run()")) + assert.True(t, strings.HasSuffix(line, "future_task.go:108")) + // + line = lines[3] + assert.Equal(t, " --- End of error stack trace ---", line) + // + line = lines[4] + assert.True(t, strings.HasPrefix(line, " created by github.com/timandy/routine.TestFutureTask_Run_Error()")) + assert.True(t, strings.HasSuffix(line, "future_task_test.go:399")) }() task.Get() assert.Fail(t, "should not be here") @@ -454,19 +455,27 @@ func TestFutureTask_Run_RuntimeError(t *testing.T) { assert.NotNil(t, cause) assert.Implements(t, (*RuntimeError)(nil), cause) err := cause.(RuntimeError) - assert.Equal(t, "", err.Message()) + assert.Equal(t, "1", err.Message()) lines := strings.Split(err.Error(), newLine) - assert.Equal(t, 11, len(lines)) + assert.Equal(t, 5, len(lines)) // line := lines[0] - assert.Equal(t, "RuntimeError", line) + assert.Equal(t, "RuntimeError: 1", line) // line = lines[1] - assert.Equal(t, line, " ---> RuntimeError: 1") + assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestFutureTask_Run_RuntimeError.")) + assert.True(t, strings.HasSuffix(line, "future_task_test.go:443")) // line = lines[2] - assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestFutureTask_Run_RuntimeError.")) - assert.True(t, strings.HasSuffix(line, "future_task_test.go:442")) + assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.(*futureTask).Run()")) + assert.True(t, strings.HasSuffix(line, "future_task.go:108")) + // + line = lines[3] + assert.Equal(t, " --- End of error stack trace ---", line) + // + line = lines[4] + assert.True(t, strings.HasPrefix(line, " created by github.com/timandy/routine.TestFutureTask_Run_RuntimeError()")) + assert.True(t, strings.HasSuffix(line, "future_task_test.go:446")) }() task.Get() assert.Fail(t, "should not be here") diff --git a/stack.go b/stack.go index 112eb2c..b52c38d 100644 --- a/stack.go +++ b/stack.go @@ -1,8 +1,34 @@ package routine -import "runtime" +import ( + "runtime" + "strings" +) + +const ( + runtimePkgPrefix = "runtime." + runtimePanic = "panic" +) func captureStackTrace(skip int, depth int) []uintptr { pcs := make([]uintptr, depth) return pcs[:runtime.Callers(skip+2, pcs)] } + +func showFrame(name string) bool { + return strings.IndexByte(name, '.') >= 0 && (!strings.HasPrefix(name, runtimePkgPrefix) || isExportedRuntime(name)) +} + +func skipFrame(name string, skipped bool) bool { + return !skipped && isPanicRuntime(name) +} + +func isExportedRuntime(name string) bool { + const n = len(runtimePkgPrefix) + return len(name) > n && name[:n] == runtimePkgPrefix && 'A' <= name[n] && name[n] <= 'Z' +} + +func isPanicRuntime(name string) bool { + const n = len(runtimePkgPrefix) + return len(name) > n && name[:n] == runtimePkgPrefix && strings.Contains(strings.ToLower(name[n:]), runtimePanic) +} diff --git a/stack_test.go b/stack_test.go index 1970956..e38d863 100644 --- a/stack_test.go +++ b/stack_test.go @@ -29,12 +29,12 @@ func TestCaptureStackTrace_Deep(t *testing.T) { frame, more := frames.Next() assert.True(t, more) assert.Equal(t, "github.com/timandy/routine.captureStackDeepRecursive", frame.Function) - assert.Equal(t, 58, frame.Line) + assert.Equal(t, 93, frame.Line) // frame2, more2 := frames.Next() assert.True(t, more2) assert.Equal(t, "github.com/timandy/routine.captureStackDeepRecursive", frame2.Function) - assert.Equal(t, 56, frame2.Line) + assert.Equal(t, 91, frame2.Line) } func TestCaptureStackTrace_Overflow(t *testing.T) { @@ -42,6 +42,41 @@ func TestCaptureStackTrace_Overflow(t *testing.T) { assert.Equal(t, 100, len(stackTrace)) } +func TestShowFrame(t *testing.T) { + assert.False(t, showFrame("make")) + assert.True(t, showFrame("strings.equal")) + assert.True(t, showFrame("strings.Equal")) + assert.False(t, showFrame("runtime.hello")) + assert.True(t, showFrame("runtime.Hello")) +} + +func TestSkipFrame(t *testing.T) { + assert.False(t, skipFrame("runtime.a", true)) + assert.False(t, skipFrame("runtime.gopanic", true)) + assert.False(t, skipFrame("runtime.a", false)) + assert.True(t, skipFrame("runtime.gopanic", false)) +} + +func TestIsExportedRuntime(t *testing.T) { + assert.False(t, isExportedRuntime("")) + assert.False(t, isExportedRuntime("runtime.")) + assert.False(t, isExportedRuntime("hello_world")) + assert.False(t, isExportedRuntime("runtime._")) + assert.False(t, isExportedRuntime("runtime.a")) + assert.True(t, isExportedRuntime("runtime.Hello")) + assert.True(t, isExportedRuntime("runtime.Panic")) +} + +func TestIsPanicRuntime(t *testing.T) { + assert.False(t, isPanicRuntime("")) + assert.False(t, isPanicRuntime("runtime.")) + assert.False(t, isPanicRuntime("hello_world")) + assert.False(t, isPanicRuntime("runtime.a")) + assert.True(t, isPanicRuntime("runtime.goPanicIndex")) + assert.True(t, isPanicRuntime("runtime.gopanic")) + assert.True(t, isPanicRuntime("runtime.panicshift")) +} + func captureStackSkip(skip int) []uintptr { return captureStackTrace(skip, 100) }