From 65c19b1b41a1b2ffff5d16780c02c33ca33c69c6 Mon Sep 17 00:00:00 2001 From: James Harris Date: Sun, 22 Nov 2020 13:46:28 +1000 Subject: [PATCH 1/3] Add `panicx.LocationOfFunc()` and `LocationOfCall()`. --- engine/panicx/linenumber_test.go | 53 +++++++++++++++++++ engine/panicx/location.go | 70 ++++++++++++++++++++----- engine/panicx/location_test.go | 35 +++++++++++++ engine/panicx/unexpectedmessage_test.go | 8 +-- 4 files changed, 146 insertions(+), 20 deletions(-) create mode 100644 engine/panicx/linenumber_test.go diff --git a/engine/panicx/linenumber_test.go b/engine/panicx/linenumber_test.go new file mode 100644 index 00000000..6e5741e7 --- /dev/null +++ b/engine/panicx/linenumber_test.go @@ -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() } diff --git a/engine/panicx/location.go b/engine/panicx/location.go index 4a8581f4..e0c0b95a 100644 --- a/engine/panicx/location.go +++ b/engine/panicx/location.go @@ -3,6 +3,7 @@ package panicx import ( "fmt" "path" + "reflect" "runtime" "strings" ) @@ -39,6 +40,45 @@ func (l Location) String() string { return "" } +// 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. // @@ -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 } @@ -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[:]) diff --git a/engine/panicx/location_test.go b/engine/panicx/location_test.go index 79c7f59b..2a2121b5 100644 --- a/engine/panicx/location_test.go +++ b/engine/panicx/location_test.go @@ -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("") + }).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", diff --git a/engine/panicx/unexpectedmessage_test.go b/engine/panicx/unexpectedmessage_test.go index ce24a3ee..7b91899f 100644 --- a/engine/panicx/unexpectedmessage_test.go +++ b/engine/panicx/unexpectedmessage_test.go @@ -109,8 +109,8 @@ 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), }, ), }, @@ -118,7 +118,3 @@ var _ = Describe("func EnrichUnexpectedMessage()", func() { )) }) }) - -func panicWithUnexpectedMessage() { - panic(dogma.UnexpectedMessage) -} From afda09519b0a88aba91d4d528c4d177044310d3a Mon Sep 17 00:00:00 2001 From: James Harris Date: Sun, 22 Nov 2020 14:14:30 +1000 Subject: [PATCH 2/3] Add `panicx.LocationOfMethod()`. --- engine/panicx/linenumber_test.go | 4 ++++ engine/panicx/location.go | 17 ++++++++++++++++- engine/panicx/location_test.go | 14 ++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/engine/panicx/linenumber_test.go b/engine/panicx/linenumber_test.go index 6e5741e7..e0476572 100644 --- a/engine/panicx/linenumber_test.go +++ b/engine/panicx/linenumber_test.go @@ -51,3 +51,7 @@ func doNothing() {} func panicWithUnexpectedMessage() { panic(dogma.UnexpectedMessage) } func locationOfCallLayer1() Location { return LocationOfCall() } func locationOfCallLayer2() Location { return locationOfCallLayer1() } + +type locationOfMethodT struct{} + +func (locationOfMethodT) Method() {} diff --git a/engine/panicx/location.go b/engine/panicx/location.go index e0c0b95a..98853fe2 100644 --- a/engine/panicx/location.go +++ b/engine/panicx/location.go @@ -42,7 +42,10 @@ func (l Location) String() string { // LocationOfFunc returns the location of the definition of fn. func LocationOfFunc(fn interface{}) Location { - rv := reflect.ValueOf(fn) + return locationOfFunc(reflect.ValueOf(fn)) +} + +func locationOfFunc(rv reflect.Value) Location { if rv.Kind() != reflect.Func { panic("fn must be a function") } @@ -60,6 +63,18 @@ func LocationOfFunc(fn interface{}) Location { 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 diff --git a/engine/panicx/location_test.go b/engine/panicx/location_test.go index 2a2121b5..1846be86 100644 --- a/engine/panicx/location_test.go +++ b/engine/panicx/location_test.go @@ -29,6 +29,20 @@ var _ = Describe("type Location", func() { }) }) + 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), + }, + )) + }) + }) + Describe("func LocationOfCall()", func() { It("returns the expected location", func() { loc := locationOfCallLayer2() From df4c361217c94478df8a5f0411f4ebb598d22c12 Mon Sep 17 00:00:00 2001 From: James Harris Date: Sun, 22 Nov 2020 14:22:33 +1000 Subject: [PATCH 3/3] Add test for `LocationOfMethod()` when given a non-existent method. --- engine/panicx/location_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/engine/panicx/location_test.go b/engine/panicx/location_test.go index 1846be86..bd9675ac 100644 --- a/engine/panicx/location_test.go +++ b/engine/panicx/location_test.go @@ -22,7 +22,7 @@ var _ = Describe("type Location", func() { )) }) - It("returns an empty location if the value is not a function", func() { + It("panics value is not a function", func() { Expect(func() { LocationOfFunc("") }).To(PanicWith("fn must be a function")) @@ -41,6 +41,12 @@ var _ = Describe("type Location", func() { }, )) }) + + It("panics if the methods does not exist", func() { + Expect(func() { + LocationOfMethod(locationOfMethodT{}, "DoesNotExist") + }).To(PanicWith("method does not exist")) + }) }) Describe("func LocationOfCall()", func() {