Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions coroutine_panic_stack_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package lua

import (
"context"
"strings"
"testing"
)

func TestResumePanicIncludesGoStack(t *testing.T) {
var logged string
L := NewState(Options{
IncludeGoStackTrace: true,
PanicHandler: func(_ *LState, message string) {
logged = message
},
})
defer L.Close()

L.SetGlobal("panic_from_go", L.NewFunction(func(*LState) int {
panic("coroutine panic sentinel")
}))
if err := L.DoString(`function run_panic() panic_from_go() end`); err != nil {
t.Fatal(err)
}

co := L.NewThreadWithContext(context.Background())
fn := L.GetGlobal("run_panic").(*LFunction)
state, _, err := L.Resume(co, fn)
if state != ResumeError {
t.Fatalf("resume state = %v, want ResumeError", state)
}
if err == nil {
t.Fatal("resume error is nil")
}
if !strings.Contains(err.Error(), "coroutine panic sentinel") {
t.Fatalf("resume error = %q, want panic message", err)
}
if !strings.Contains(err.Error(), "TestResumePanicIncludesGoStack") {
t.Fatalf("resume error = %q, want Go stack", err)
}
if logged != err.Error() {
t.Fatalf("panic handler message = %q, want returned error", logged)
}
}

func TestResumePanicHandlerPanicKeepsOriginalError(t *testing.T) {
L := NewState(Options{
PanicHandler: func(*LState, string) {
panic("panic handler sentinel")
},
})
defer L.Close()

L.SetGlobal("panic_from_go", L.NewFunction(func(*LState) int {
panic("coroutine panic sentinel")
}))
if err := L.DoString(`function run_panic() panic_from_go() end`); err != nil {
t.Fatal(err)
}

co := L.NewThreadWithContext(context.Background())
fn := L.GetGlobal("run_panic").(*LFunction)
state, _, err := L.Resume(co, fn)
if state != ResumeError {
t.Fatalf("resume state = %v, want ResumeError", state)
}
if err == nil || !strings.Contains(err.Error(), "coroutine panic sentinel") {
t.Fatalf("resume error = %v, want original panic", err)
}
if strings.Contains(err.Error(), "panic handler sentinel") {
t.Fatalf("resume error = %q, want no handler panic", err)
}
if !co.Dead {
t.Fatal("coroutine is not dead")
}
if status := L.Status(co); status != "dead" {
t.Fatalf("coroutine status = %q, want dead", status)
}
}
1 change: 1 addition & 0 deletions state.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ type Options struct {
SkipOpenLibs bool
// Tells whether a Go stacktrace should be included in a Lua stacktrace when panics occur.
IncludeGoStackTrace bool
PanicHandler func(*LState, string)
// If `MinimizeStackMemory` is set, the call stack will be automatically grown or shrank up to a limit of
// `CallStackSize` in order to minimize memory usage. This does incur a slight performance penalty.
MinimizeStackMemory bool
Expand Down
18 changes: 17 additions & 1 deletion vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"math"
"runtime/debug"
"strings"
"sync"
)
Expand Down Expand Up @@ -3135,7 +3136,12 @@ func threadRun(L *LState) {
SetErrorMetatable(L, e)
}
} else {
lv = LString(fmt.Sprint(rcv))
message := fmt.Sprint(rcv)
if L.Options.IncludeGoStackTrace {
message += "\n" + string(debug.Stack())
}
reportPanic(L, message)
lv = LString(message)
}

// Check if there's a protected frame that should catch this error
Expand Down Expand Up @@ -3163,6 +3169,16 @@ func threadRun(L *LState) {
L.mainLoop(L, nil)
}

func reportPanic(L *LState, message string) {
if L.Options.PanicHandler == nil {
return
}
defer func() {
_ = recover()
}()
L.Options.PanicHandler(L, message)
}

// handleProtectedError searches for a protected (pcall) frame and handles the error.
// Returns true if error was handled, false if it should propagate.
func handleProtectedError(L *LState, errValue LValue, _ interface{}) bool {
Expand Down
Loading