diff --git a/engine/controller/aggregate/controller.go b/engine/controller/aggregate/controller.go index c86b1270..a6bd32c9 100644 --- a/engine/controller/aggregate/controller.go +++ b/engine/controller/aggregate/controller.go @@ -10,6 +10,7 @@ import ( "github.com/dogmatiq/testkit/engine/envelope" "github.com/dogmatiq/testkit/engine/fact" "github.com/dogmatiq/testkit/engine/panicx" + "github.com/dogmatiq/testkit/internal/location" ) // Controller is an implementation of engine.Controller for @@ -65,7 +66,7 @@ func (c *Controller) Handle( Implementation: c.Config.Handler(), Message: env.Message, Description: fmt.Sprintf("routed a command of type %T to an empty instance ID", env.Message), - Location: panicx.LocationOfMethod(c.Config.Handler(), "RouteCommandToInstance"), + Location: location.OfMethod(c.Config.Handler(), "RouteCommandToInstance"), }) } @@ -79,7 +80,7 @@ func (c *Controller) Handle( Implementation: c.Config.Handler(), Message: env.Message, Description: "returned a nil AggregateRoot", - Location: panicx.LocationOfMethod(c.Config.Handler(), "New"), + Location: location.OfMethod(c.Config.Handler(), "New"), }) } diff --git a/engine/controller/aggregate/scope.go b/engine/controller/aggregate/scope.go index cd037626..6f5d0c2e 100644 --- a/engine/controller/aggregate/scope.go +++ b/engine/controller/aggregate/scope.go @@ -9,6 +9,7 @@ import ( "github.com/dogmatiq/testkit/engine/envelope" "github.com/dogmatiq/testkit/engine/fact" "github.com/dogmatiq/testkit/engine/panicx" + "github.com/dogmatiq/testkit/internal/location" ) // scope is an implementation of dogma.AggregateCommandScope. @@ -53,7 +54,7 @@ func (s *scope) RecordEvent(m dogma.Message) { Implementation: s.config.Handler(), Message: s.command.Message, Description: fmt.Sprintf("recorded an event of type %T, which is not produced by this handler", m), - Location: panicx.LocationOfCall(), + Location: location.OfCall(), }) } @@ -65,7 +66,7 @@ func (s *scope) RecordEvent(m dogma.Message) { Implementation: s.config.Handler(), Message: s.command.Message, Description: fmt.Sprintf("recorded an invalid %T event: %s", m, err), - Location: panicx.LocationOfCall(), + Location: location.OfCall(), }) } diff --git a/engine/controller/integration/scope.go b/engine/controller/integration/scope.go index 345e510b..dbd115cb 100644 --- a/engine/controller/integration/scope.go +++ b/engine/controller/integration/scope.go @@ -9,6 +9,7 @@ import ( "github.com/dogmatiq/testkit/engine/envelope" "github.com/dogmatiq/testkit/engine/fact" "github.com/dogmatiq/testkit/engine/panicx" + "github.com/dogmatiq/testkit/internal/location" ) // scope is an implementation of dogma.IntegrationCommandScope. @@ -30,7 +31,7 @@ func (s *scope) RecordEvent(m dogma.Message) { Implementation: s.config.Handler(), Message: s.command.Message, Description: fmt.Sprintf("recorded an event of type %T, which is not produced by this handler", m), - Location: panicx.LocationOfCall(), + Location: location.OfCall(), }) } @@ -42,7 +43,7 @@ func (s *scope) RecordEvent(m dogma.Message) { Implementation: s.config.Handler(), Message: s.command.Message, Description: fmt.Sprintf("recorded an invalid %T event: %s", m, err), - Location: panicx.LocationOfCall(), + Location: location.OfCall(), }) } diff --git a/engine/controller/process/controller.go b/engine/controller/process/controller.go index af3f1d79..21ea5d8e 100644 --- a/engine/controller/process/controller.go +++ b/engine/controller/process/controller.go @@ -12,6 +12,7 @@ import ( "github.com/dogmatiq/testkit/engine/envelope" "github.com/dogmatiq/testkit/engine/fact" "github.com/dogmatiq/testkit/engine/panicx" + "github.com/dogmatiq/testkit/internal/location" ) // Controller is an implementation of engine.Controller for @@ -114,7 +115,7 @@ func (c *Controller) Handle( Implementation: c.Config.Handler(), Message: env.Message, Description: "returned a nil ProcessRoot", - Location: panicx.LocationOfMethod(c.Config.Handler(), "New"), + Location: location.OfMethod(c.Config.Handler(), "New"), }) } } @@ -203,7 +204,7 @@ func (c *Controller) routeEvent( Implementation: handler, Message: env.Message, Description: fmt.Sprintf("routed an event of type %T to an empty instance ID", env.Message), - Location: panicx.LocationOfMethod(c.Config.Handler(), "RouteEventToInstance"), + Location: location.OfMethod(c.Config.Handler(), "RouteEventToInstance"), }) } diff --git a/engine/controller/process/scope.go b/engine/controller/process/scope.go index 8bcc7edc..e4140d48 100644 --- a/engine/controller/process/scope.go +++ b/engine/controller/process/scope.go @@ -10,6 +10,7 @@ import ( "github.com/dogmatiq/testkit/engine/envelope" "github.com/dogmatiq/testkit/engine/fact" "github.com/dogmatiq/testkit/engine/panicx" + "github.com/dogmatiq/testkit/internal/location" ) // scope is an implementation of dogma.ProcessEventScope and @@ -63,7 +64,7 @@ func (s *scope) End() { Message: s.env.Message, Implementation: s.config.Handler(), Description: "ended a process instance that has not begun", - Location: panicx.LocationOfCall(), + Location: location.OfCall(), }) } @@ -88,7 +89,7 @@ func (s *scope) Root() dogma.ProcessRoot { Message: s.env.Message, Implementation: s.config.Handler(), Description: "accessed the root of a process instance that has not begun", - Location: panicx.LocationOfCall(), + Location: location.OfCall(), }) } @@ -104,7 +105,7 @@ func (s *scope) ExecuteCommand(m dogma.Message) { Implementation: s.config.Handler(), Message: s.env.Message, Description: fmt.Sprintf("executed a command of type %T, which is not produced by this handler", m), - Location: panicx.LocationOfCall(), + Location: location.OfCall(), }) } @@ -116,7 +117,7 @@ func (s *scope) ExecuteCommand(m dogma.Message) { Message: s.env.Message, Implementation: s.config.Handler(), Description: fmt.Sprintf("executed an invalid %T command: %s", m, err), - Location: panicx.LocationOfCall(), + Location: location.OfCall(), }) } @@ -128,7 +129,7 @@ func (s *scope) ExecuteCommand(m dogma.Message) { Message: s.env.Message, Implementation: s.config.Handler(), Description: fmt.Sprintf("executed a command of type %T on a process instance that has not begun", m), - Location: panicx.LocationOfCall(), + Location: location.OfCall(), }) } @@ -167,7 +168,7 @@ func (s *scope) ScheduleTimeout(m dogma.Message, t time.Time) { Implementation: s.config.Handler(), Message: s.env.Message, Description: fmt.Sprintf("scheduled a timeout of type %T, which is not produced by this handler", m), - Location: panicx.LocationOfCall(), + Location: location.OfCall(), }) } @@ -179,7 +180,7 @@ func (s *scope) ScheduleTimeout(m dogma.Message, t time.Time) { Message: s.env.Message, Implementation: s.config.Handler(), Description: fmt.Sprintf("scheduled an invalid %T timeout: %s", m, err), - Location: panicx.LocationOfCall(), + Location: location.OfCall(), }) } @@ -191,7 +192,7 @@ func (s *scope) ScheduleTimeout(m dogma.Message, t time.Time) { Message: s.env.Message, Implementation: s.config.Handler(), Description: fmt.Sprintf("scheduled a timeout of type %T on a process instance that has not begun", m), - Location: panicx.LocationOfCall(), + Location: location.OfCall(), }) } diff --git a/engine/panicx/linenumber_test.go b/engine/panicx/linenumber_test.go index e0476572..53d9a7d0 100644 --- a/engine/panicx/linenumber_test.go +++ b/engine/panicx/linenumber_test.go @@ -10,7 +10,7 @@ package panicx_test import ( "github.com/dogmatiq/dogma" - . "github.com/dogmatiq/testkit/engine/panicx" + // import padding // import padding // import padding // import padding @@ -47,11 +47,4 @@ import ( // 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() {} +func doPanic() { panic(dogma.UnexpectedMessage) } diff --git a/engine/panicx/unexpectedbehavior.go b/engine/panicx/unexpectedbehavior.go index 7b3ce96c..6446aef7 100644 --- a/engine/panicx/unexpectedbehavior.go +++ b/engine/panicx/unexpectedbehavior.go @@ -5,6 +5,7 @@ import ( "github.com/dogmatiq/configkit" "github.com/dogmatiq/dogma" + "github.com/dogmatiq/testkit/internal/location" ) // UnexpectedBehavior is a panic value that occurs when a handler exhibits some @@ -33,7 +34,7 @@ type UnexpectedBehavior struct { // Location is the engine's best attempt at pinpointing the location of the // unexpected behavior. - Location Location + Location location.Location } func (x UnexpectedBehavior) String() string { diff --git a/engine/panicx/unexpectedmessage.go b/engine/panicx/unexpectedmessage.go index dafbf5b4..418c54fa 100644 --- a/engine/panicx/unexpectedmessage.go +++ b/engine/panicx/unexpectedmessage.go @@ -5,6 +5,7 @@ import ( "github.com/dogmatiq/configkit" "github.com/dogmatiq/dogma" + "github.com/dogmatiq/testkit/internal/location" ) // UnexpectedMessage is a panic value that provides more context when a handler @@ -28,7 +29,7 @@ type UnexpectedMessage struct { Message dogma.Message // PanicLocation is the location of the function that panicked, if known. - PanicLocation Location + PanicLocation location.Location } func (x UnexpectedMessage) String() string { @@ -66,7 +67,7 @@ func EnrichUnexpectedMessage( Method: method, Implementation: impl, Message: m, - PanicLocation: LocationOfPanic(), + PanicLocation: location.OfPanic(), } } diff --git a/engine/panicx/unexpectedmessage_test.go b/engine/panicx/unexpectedmessage_test.go index 7b91899f..7caead6d 100644 --- a/engine/panicx/unexpectedmessage_test.go +++ b/engine/panicx/unexpectedmessage_test.go @@ -96,7 +96,7 @@ var _ = Describe("func EnrichUnexpectedMessage()", func() { "", config.Handler(), MessageA1, - panicWithUnexpectedMessage, + doPanic, ) }).To(PanicWith( MatchAllFields( @@ -108,9 +108,9 @@ var _ = Describe("func EnrichUnexpectedMessage()", func() { "Message": Equal(MessageA1), "PanicLocation": MatchAllFields( Fields{ - "Func": Equal("github.com/dogmatiq/testkit/engine/panicx_test.panicWithUnexpectedMessage"), + "Func": Equal("github.com/dogmatiq/testkit/engine/panicx_test.doPanic"), "File": HaveSuffix("/engine/panicx/linenumber_test.go"), - "Line": Equal(51), + "Line": Equal(50), }, ), }, diff --git a/internal/location/doc.go b/internal/location/doc.go new file mode 100644 index 00000000..dd0635af --- /dev/null +++ b/internal/location/doc.go @@ -0,0 +1,3 @@ +// Package location contains utilities for obtaining the function, file and line +// number of various Go types and values. +package location diff --git a/internal/location/ginkgo_test.go b/internal/location/ginkgo_test.go new file mode 100644 index 00000000..76114503 --- /dev/null +++ b/internal/location/ginkgo_test.go @@ -0,0 +1,15 @@ +package location_test + +import ( + "reflect" + "testing" + + "github.com/onsi/ginkgo" + "github.com/onsi/gomega" +) + +func TestSuite(t *testing.T) { + type tag struct{} + gomega.RegisterFailHandler(ginkgo.Fail) + ginkgo.RunSpecs(t, reflect.TypeOf(tag{}).PkgPath()) +} diff --git a/internal/location/linenumber_test.go b/internal/location/linenumber_test.go new file mode 100644 index 00000000..d0d545a9 --- /dev/null +++ b/internal/location/linenumber_test.go @@ -0,0 +1,57 @@ +package location_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/testkit/internal/location" + // 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 + // import padding +) + +func doNothing() {} +func doPanic() { panic("") } +func ofCallLayer1() Location { return OfCall() } +func ofCallLayer2() Location { return ofCallLayer1() } + +type ofMethodT struct{} + +func (ofMethodT) Method() {} diff --git a/engine/panicx/location.go b/internal/location/location.go similarity index 75% rename from engine/panicx/location.go rename to internal/location/location.go index 98853fe2..9b889112 100644 --- a/engine/panicx/location.go +++ b/internal/location/location.go @@ -1,4 +1,4 @@ -package panicx +package location import ( "fmt" @@ -8,10 +8,21 @@ import ( "strings" ) -// Location describes a location within the codebase. +// Location describes a location within a code base. +// +// Any of the fields may be zero-valued, indicating that piece of information is +// unknown. type Location struct { + // Func is the fully-qualified name of the function. + // + // The format of this string is not defined by this package and is subject + // to change. Func string + + // File is the name of the Go source file. File string + + // Line is the line number within the file, starting at 1. Line int } @@ -20,7 +31,7 @@ func (l Location) String() string { return fmt.Sprintf( "%s() %s:%d", l.Func, - l.File, //path.Base(l.File), + path.Base(l.File), l.Line, ) } @@ -40,12 +51,12 @@ func (l Location) String() string { return "" } -// LocationOfFunc returns the location of the definition of fn. -func LocationOfFunc(fn interface{}) Location { - return locationOfFunc(reflect.ValueOf(fn)) +// OfFunc returns the location of the definition of fn. +func OfFunc(fn interface{}) Location { + return ofFunc(reflect.ValueOf(fn)) } -func locationOfFunc(rv reflect.Value) Location { +func ofFunc(rv reflect.Value) Location { if rv.Kind() != reflect.Func { panic("fn must be a function") } @@ -63,8 +74,8 @@ func locationOfFunc(rv reflect.Value) Location { return loc } -// LocationOfMethod returns the location of the definition of fn. -func LocationOfMethod(recv interface{}, m string) Location { +// OfMethod returns the location of the definition of fn. +func OfMethod(recv interface{}, m string) Location { rt := reflect.TypeOf(recv) rm, ok := rt.MethodByName(m) @@ -72,11 +83,11 @@ func LocationOfMethod(recv interface{}, m string) Location { panic("method does not exist") } - return locationOfFunc(rm.Func) + return ofFunc(rm.Func) } -// LocationOfCall returns the location where its caller was called itself. -func LocationOfCall() Location { +// OfCall returns the location where its caller was called itself. +func OfCall() Location { var loc Location eachFrame( @@ -94,12 +105,12 @@ func LocationOfCall() Location { return loc } -// LocationOfPanic returns the location of the call to panic() that caused the +// OfPanic returns the location of the call to panic() that caused the // stack to start unwinding. // // It must be called within a deferred function and only if recover() returned a // non-nil value. Otherwise the behavior of the function is undefined. -func LocationOfPanic() Location { +func OfPanic() Location { // During a panic() the runtime *adds* frames for each deferred function, so // the function that caused the panic is still on the stack, even though it // is unwinding. diff --git a/engine/panicx/location_test.go b/internal/location/location_test.go similarity index 52% rename from engine/panicx/location_test.go rename to internal/location/location_test.go index fd1de851..70a36676 100644 --- a/engine/panicx/location_test.go +++ b/internal/location/location_test.go @@ -1,7 +1,7 @@ -package panicx_test +package location_test import ( - . "github.com/dogmatiq/testkit/engine/panicx" + . "github.com/dogmatiq/testkit/internal/location" . "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" @@ -9,14 +9,14 @@ import ( ) var _ = Describe("type Location", func() { - Describe("func LocationOfFunc()", func() { + Describe("func OfFunc()", func() { It("returns the expected location", func() { - loc := LocationOfFunc(doNothing) + loc := OfFunc(doNothing) Expect(loc).To(MatchAllFields( Fields{ - "Func": Equal("github.com/dogmatiq/testkit/engine/panicx_test.doNothing"), - "File": HaveSuffix("/engine/panicx/linenumber_test.go"), + "Func": Equal("github.com/dogmatiq/testkit/internal/location_test.doNothing"), + "File": HaveSuffix("/internal/location/linenumber_test.go"), "Line": Equal(50), }, )) @@ -24,19 +24,19 @@ var _ = Describe("type Location", func() { It("panics if the value is not a function", func() { Expect(func() { - LocationOfFunc("") + OfFunc("") }).To(PanicWith("fn must be a function")) }) }) - Describe("func LocationOfMethod()", func() { + Describe("func OfMethod()", func() { It("returns the expected location", func() { - loc := LocationOfMethod(locationOfMethodT{}, "Method") + loc := OfMethod(ofMethodT{}, "Method") Expect(loc).To(MatchAllFields( Fields{ - "Func": Equal("github.com/dogmatiq/testkit/engine/panicx_test.locationOfMethodT.Method"), - "File": HaveSuffix("/engine/panicx/linenumber_test.go"), + "Func": Equal("github.com/dogmatiq/testkit/internal/location_test.ofMethodT.Method"), + "File": HaveSuffix("/internal/location/linenumber_test.go"), "Line": Equal(57), }, )) @@ -44,25 +44,44 @@ var _ = Describe("type Location", func() { It("panics if the methods does not exist", func() { Expect(func() { - LocationOfMethod(locationOfMethodT{}, "DoesNotExist") + OfMethod(ofMethodT{}, "DoesNotExist") }).To(PanicWith("method does not exist")) }) }) - Describe("func LocationOfCall()", func() { + Describe("func OfCall()", func() { It("returns the expected location", func() { - loc := locationOfCallLayer2() + loc := ofCallLayer2() Expect(loc).To(MatchAllFields( Fields{ - "Func": Equal("github.com/dogmatiq/testkit/engine/panicx_test.locationOfCallLayer2"), - "File": HaveSuffix("/engine/panicx/linenumber_test.go"), + "Func": Equal("github.com/dogmatiq/testkit/internal/location_test.ofCallLayer2"), + "File": HaveSuffix("/internal/location/linenumber_test.go"), "Line": Equal(53), }, )) }) }) + Describe("func OfPanic()", func() { + It("returns the expected location", func() { + defer func() { + recover() + loc := OfPanic() + + Expect(loc).To(MatchAllFields( + Fields{ + "Func": Equal("github.com/dogmatiq/testkit/internal/location_test.doPanic"), + "File": HaveSuffix("/internal/location/linenumber_test.go"), + "Line": Equal(51), + }, + )) + }() + + doPanic() + }) + }) + Describe("func String()", func() { DescribeTable( "it returns the expected string",