Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add functions for getting the location of functions and calls. #177

Merged
merged 3 commits into from
Nov 22, 2020
Merged
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
57 changes: 57 additions & 0 deletions engine/panicx/linenumber_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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() }

type locationOfMethodT struct{}

func (locationOfMethodT) Method() {}
85 changes: 71 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,60 @@ func (l Location) String() string {
return "<unknown>"
}

// LocationOfFunc returns the location of the definition of fn.
func LocationOfFunc(fn interface{}) Location {
return locationOfFunc(reflect.ValueOf(fn))
}

func locationOfFunc(rv reflect.Value) Location {
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
}

// LocationOfMethod returns the location of the definition of fn.
func LocationOfMethod(recv interface{}, m string) Location {
rt := reflect.TypeOf(recv)

rm, ok := rt.MethodByName(m)
if !ok {
panic("method does not exist")
}

return locationOfFunc(rm.Func)
}

// 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 +111,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 +152,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
55 changes: 55 additions & 0 deletions engine/panicx/location_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,64 @@ 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("panics value is not a function", func() {
Expect(func() {
LocationOfFunc("<not a function>")
}).To(PanicWith("fn must be a function"))
})
})

Describe("func LocationOfMethod()", func() {
It("returns the expected location", func() {
loc := LocationOfMethod(locationOfMethodT{}, "Method")

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

It("panics if the methods does not exist", func() {
Expect(func() {
LocationOfMethod(locationOfMethodT{}, "DoesNotExist")
}).To(PanicWith("method does not exist"))
})
})

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