Skip to content

Commit

Permalink
Add panicx.LocationOfFunc() and LocationOfCall().
Browse files Browse the repository at this point in the history
  • Loading branch information
jmalloc committed Nov 22, 2020
1 parent a20640f commit 9b49462
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 20 deletions.
53 changes: 53 additions & 0 deletions engine/panicx/linenumber_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package panicx_test

// This file contains definitions used within tests that check for specific line
// numbers. To minimize test disruption edit this file as infrequently as
// possible.
//
// New additions should always be made at the end so that the line numbers of
// existing definitions do not change. The padding below can be removed as
// imports statements added.

import (
"github.com/dogmatiq/dogma"
. "github.com/dogmatiq/testkit/engine/panicx"
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
// import padding
)

func doNothing() {}
func panicWithUnexpectedMessage() { panic(dogma.UnexpectedMessage) }
func locationOfCallLayer1() Location { return LocationOfCall() }
func locationOfCallLayer2() Location { return locationOfCallLayer1() }
70 changes: 56 additions & 14 deletions engine/panicx/location.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package panicx
import (
"fmt"
"path"
"reflect"
"runtime"
"strings"
)
Expand Down Expand Up @@ -39,6 +40,45 @@ func (l Location) String() string {
return "<unknown>"
}

// LocationOfFunc returns the location of the definition of fn.
func LocationOfFunc(fn interface{}) Location {
rv := reflect.ValueOf(fn)
if rv.Kind() != reflect.Func {
panic("fn must be a function")
}

var (
loc Location
pc = rv.Pointer()
)

if fn := runtime.FuncForPC(pc); fn != nil {
loc.Func = fn.Name()
loc.File, loc.Line = fn.FileLine(pc)
}

return loc
}

// LocationOfCall returns the location where its caller was called itself.
func LocationOfCall() Location {
var loc Location

eachFrame(
2, // skip LocationOfCall() and its caller.
func(fr runtime.Frame) bool {
loc = Location{
Func: fr.Function,
File: fr.File,
Line: fr.Line,
}
return false
},
)

return loc
}

// LocationOfPanic returns the location of the call to panic() that caused the
// stack to start unwinding.
//
Expand All @@ -56,20 +96,22 @@ func LocationOfPanic() Location {
foundPanicCall := false

eachFrame(
0,
func(fr runtime.Frame) bool {
if fr.Function == "runtime.gopanic" {
// We found the call to runtime.gopanic(), which is the internal
// implementation of panic().
//
// That means that the next function we find that's NOT in the
// "runtime" package is the function that called panic().
foundPanicCall = true
return true
}

if strings.HasPrefix(fr.Function, "runtime.") {
// We found some other function within the runtime package, we
// keep looking for some user-land code.
// We found some function within the runtime package, we keep
// looking for some user-land code.

if fr.Function == "runtime.gopanic" {
// We found the call to runtime.gopanic(), which is the
// internal implementation of panic().
//
// That means that the next function we find that's NOT in
// the "runtime" package is the function that called
// panic().
foundPanicCall = true
}

return true
}

Expand All @@ -95,9 +137,9 @@ func LocationOfPanic() Location {
}

// eachFrame calls fn for each frame on the call stack until fn returns false.
func eachFrame(fn func(fr runtime.Frame) bool) {
func eachFrame(skip int, fn func(fr runtime.Frame) bool) {
var pointers [8]uintptr
var skip int
skip += 2 // Always skip runtime.Callers() and eachFrame().

for {
count := runtime.Callers(skip, pointers[:])
Expand Down
35 changes: 35 additions & 0 deletions engine/panicx/location_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,44 @@ import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gstruct"
)

var _ = Describe("type Location", func() {
Describe("func LocationOfFunc()", func() {
It("returns the expected location", func() {
loc := LocationOfFunc(doNothing)

Expect(loc).To(MatchAllFields(
Fields{
"Func": Equal("github.com/dogmatiq/testkit/engine/panicx_test.doNothing"),
"File": HaveSuffix("/engine/panicx/linenumber_test.go"),
"Line": Equal(50),
},
))
})

It("returns an empty location if the value is not a function", func() {
Expect(func() {
LocationOfFunc("<not a function>")
}).To(PanicWith("fn must be a function"))
})
})

Describe("func LocationOfCall()", func() {
It("returns the expected location", func() {
loc := locationOfCallLayer2()

Expect(loc).To(MatchAllFields(
Fields{
"Func": Equal("github.com/dogmatiq/testkit/engine/panicx_test.locationOfCallLayer2"),
"File": HaveSuffix("/engine/panicx/linenumber_test.go"),
"Line": Equal(53),
},
))
})
})

Describe("func String()", func() {
DescribeTable(
"it returns the expectation string",
Expand Down
48 changes: 48 additions & 0 deletions engine/panicx/unexpectedbehavior.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package panicx

import (
"fmt"

"github.com/dogmatiq/configkit"
"github.com/dogmatiq/dogma"
)

// UnexpectedBehavior is a panic value that occurs when a handler exhibits some
// behavior that the engine did not expect.
//
// Often this means it has violated the Dogma specification.
type UnexpectedBehavior struct {
// Handler is the non-compliant handler.
Handler configkit.RichHandler

// Interface is the name of the interface containing the method with the
// unexpected behavior.
Interface string

// Method is the name of the method that behaved unexpectedly.
Method string

// Implementation is the value that implements the nominated interface.
Implementation interface{}

// Message is the message that was being handled at the time, if any.
Message dogma.Message

// Description is a human-readable description of the behavior.
Description string

// Location is the engine's best attempt at pinpointing the location of the
// unexpected behavior.
Location Location
}

func (x UnexpectedBehavior) String() string {
return fmt.Sprintf(
"the '%s' %s message handler behaved unexpectedly in %T.%s(): %s",
x.Handler.Identity().Name,
x.Handler.HandlerType(),
x.Implementation,
x.Method,
x.Description,
)
}
37 changes: 37 additions & 0 deletions engine/panicx/unexpectedbehavior_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package panicx_test

import (
"github.com/dogmatiq/configkit"
"github.com/dogmatiq/dogma"
. "github.com/dogmatiq/dogma/fixtures"
. "github.com/dogmatiq/testkit/engine/panicx"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("type UnexpectedBehavior", func() {
config := configkit.FromProjection(
&ProjectionMessageHandler{
ConfigureFunc: func(c dogma.ProjectionConfigurer) {
c.Identity("<name>", "<key>")
c.ConsumesEventType(MessageE{})
},
},
)

Describe("func String()", func() {
It("returns a description of the panic", func() {
x := UnexpectedBehavior{
Handler: config,
Interface: "<interface>",
Method: "<method>",
Implementation: config.Handler(),
Description: "<description>",
}

Expect(x.String()).To(Equal(
"the '<name>' projection message handler behaved unexpectedly in *fixtures.ProjectionMessageHandler.<method>(): <description>",
))
})
})
})
8 changes: 2 additions & 6 deletions engine/panicx/unexpectedmessage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,16 +109,12 @@ var _ = Describe("func EnrichUnexpectedMessage()", func() {
"PanicLocation": MatchAllFields(
Fields{
"Func": Equal("github.com/dogmatiq/testkit/engine/panicx_test.panicWithUnexpectedMessage"),
"File": HaveSuffix("/engine/panicx/unexpectedmessage_test.go"),
"Line": Equal(123),
"File": HaveSuffix("/engine/panicx/linenumber_test.go"),
"Line": Equal(51),
},
),
},
),
))
})
})

func panicWithUnexpectedMessage() {
panic(dogma.UnexpectedMessage)
}

0 comments on commit 9b49462

Please sign in to comment.