Skip to content

Commit

Permalink
Merge pull request #350 from dogmatiq/annotate
Browse files Browse the repository at this point in the history
Add custom value annotations to test reports.
  • Loading branch information
jmalloc authored Aug 20, 2024
2 parents fd4e4eb + 08270b5 commit 90513df
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 66 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ The format is based on [Keep a Changelog], and this project adheres to

- Added `ReportGenerationContext` to hold information required when generating
test reports.
- Added `Test.Annotate()` to add human-readable annotations to values within
test reports.

### Changed

Expand Down
19 changes: 12 additions & 7 deletions compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,7 @@ type MessageComparator func(a, b dogma.Message) bool
// It supports comparison of protocol buffers messages using the proto.Equal()
// function. All other types are compared using reflect.DeepEqual().
func DefaultMessageComparator(a, b dogma.Message) bool {
if pa, ok := a.(proto.Message); ok {
if pb, ok := b.(proto.Message); ok {
return proto.Equal(pa, pb)
}
}

return reflect.DeepEqual(a, b)
return equal(a, b)
}

// WithMessageComparator returns a test option that sets the comparator
Expand All @@ -38,3 +32,14 @@ func WithMessageComparator(c MessageComparator) TestOption {
t.predicateOptions.MessageComparator = c
})
}

// equal returns true if a and b are considered equal.
func equal(a, b any) bool {
if pa, ok := a.(proto.Message); ok {
if pb, ok := b.(proto.Message); ok {
return proto.Equal(pa, pb)
}
}

return reflect.DeepEqual(a, b)
}
8 changes: 4 additions & 4 deletions expectation.message.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,16 +198,16 @@ func (p *messagePredicate) Report(ctx ReportGenerationContext) *Report {
s.AppendListItem("check the message type, should it be a pointer?")
}

p.buildDiff(rep)
p.buildDiff(ctx, rep)

return rep
}

// buildDiff adds a "message diff" section to the result.
func (p *messagePredicate) buildDiff(rep *Report) {
func (p *messagePredicate) buildDiff(ctx ReportGenerationContext, rep *Report) {
report.WriteDiff(
&rep.Section("Message Diff").Content,
report.RenderMessage(p.expectedMessage),
report.RenderMessage(p.bestMatch.Message),
ctx.renderMessage(p.expectedMessage),
ctx.renderMessage(p.bestMatch.Message),
)
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ toolchain go1.22.3
require (
github.com/dogmatiq/configkit v0.13.6
github.com/dogmatiq/cosyne v0.2.0
github.com/dogmatiq/dapper v0.5.3
github.com/dogmatiq/dapper v0.6.0
github.com/dogmatiq/dogma v0.14.1
github.com/dogmatiq/iago v0.4.0
github.com/dogmatiq/linger v1.1.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ github.com/dogmatiq/configkit v0.13.6 h1:tgMl55VC9ra1NjMfZJIyHTInTi/mTh8AScTpasI
github.com/dogmatiq/configkit v0.13.6/go.mod h1:wjuets0+wsD/7pYFE9bX2YvZ6LlrEx330yBG1nXoDSw=
github.com/dogmatiq/cosyne v0.2.0 h1:tO957BpS4I9kqSw31ds6Ef4CXvV8zPAqWzbXKElsGWg=
github.com/dogmatiq/cosyne v0.2.0/go.mod h1:dD8EZjbRX7FFw9t6P7l1nwoZbA7YxtOCfl9ZZAHPucU=
github.com/dogmatiq/dapper v0.5.3 h1:DZkitO0TiokaiZt+9J7UNnagW2ezSYmJUlDTXLWGf8g=
github.com/dogmatiq/dapper v0.5.3/go.mod h1:lrBXvNri2wXkk1T0muaTUqd5lVDwIBRKeOzVRU46XI0=
github.com/dogmatiq/dapper v0.6.0 h1:hnWUsjnt3nUiC9hmkPvuxrnMd7fYNz1i+/GS3gOx0Xs=
github.com/dogmatiq/dapper v0.6.0/go.mod h1:ubRHWzt73s0MsPpGhWvnfW/Z/1YPnrkCsQv6CUOZVEw=
github.com/dogmatiq/dogma v0.14.1 h1:uSYkfDR9Wr7zCphDIZBZpKOr1QLD4UpTJy5vD+lDFpg=
github.com/dogmatiq/dogma v0.14.1/go.mod h1:9lyVA+6V2+E/exV0IrBOrkUiyFwIATEhv+b0vnB2umQ=
github.com/dogmatiq/iago v0.4.0 h1:57nZqVT34IZxtCZEW/RFif7DNUEjMXgevfr/Mmd0N8I=
Expand Down
25 changes: 0 additions & 25 deletions internal/report/message.go

This file was deleted.

26 changes: 0 additions & 26 deletions internal/report/message_test.go

This file was deleted.

15 changes: 15 additions & 0 deletions report.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"io"
"strings"

"github.com/dogmatiq/dapper"
"github.com/dogmatiq/dogma"
"github.com/dogmatiq/iago/count"
"github.com/dogmatiq/iago/indent"
"github.com/dogmatiq/iago/must"
Expand All @@ -24,6 +26,13 @@ const (
failedMatchesSection = "Failed Matches"
)

// Annotation is a textual description of a value that provides additional
// context in test reports.
type Annotation struct {
Value any
Text string
}

// ReportGenerationContext is the context in which a report is generated.
type ReportGenerationContext struct {
// TreeOk is true if the entire "tree" of expectations is considered to have
Expand All @@ -33,6 +42,12 @@ type ReportGenerationContext struct {
// IsInverted is true if the expectation is inverted, i.e. it is expected
// NOT to be met.
IsInverted bool

printer *dapper.Printer
}

func (c ReportGenerationContext) renderMessage(m dogma.Message) string {
return c.printer.Format(m)
}

// Report is a report on the outcome of an expectation.
Expand Down
45 changes: 44 additions & 1 deletion test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package testkit
import (
"context"
"fmt"
"reflect"
"regexp"
"strings"
"time"

"github.com/dogmatiq/configkit"
"github.com/dogmatiq/dapper"
"github.com/dogmatiq/dogma"
"github.com/dogmatiq/iago/must"
"github.com/dogmatiq/testkit/engine"
Expand All @@ -26,6 +28,7 @@ type Test struct {
executor CommandExecutor
predicateOptions PredicateOptions
operationOptions []engine.OperationOption
annotations []Annotation
}

// Begin starts a new test.
Expand Down Expand Up @@ -124,9 +127,41 @@ func (t *Test) Expect(act Action, e Expectation) *Test {
return t // required when using a mock testingT that does not panic
}

options := []dapper.Option{
dapper.WithPackagePaths(false),
dapper.WithUnexportedStructFields(false),
}

for _, a := range t.annotations {
rt := reflect.TypeOf(a.Value)

options = append(
options,
dapper.WithAnnotator(
func(v dapper.Value) string {
// Check that the types are EXACT, otherwise the annotation
// can be duplicated, for example, once when boxed in an
// interface, and again when descending into that boxed
// value.
if rt != v.Value.Type() {
return ""
}

if !equal(a.Value, v.Value.Interface()) {
return ""
}

return a.Text
},
),
)
}

ctx := ReportGenerationContext{
TreeOk: p.Ok(),
TreeOk: p.Ok(),
printer: dapper.NewPrinter(options...),
}

rep := p.Report(ctx)

buf := &strings.Builder{}
Expand All @@ -153,6 +188,14 @@ func (t *Test) CommandExecutor() dogma.CommandExecutor {
return &t.executor
}

// Annotate adds an annotation to v.
//
// The annotation text is displayed whenever v is rendered in a test report.
func (t *Test) Annotate(v any, text string) *Test {
t.annotations = append(t.annotations, Annotation{v, text})
return t
}

// EnableHandlers enables a set of handlers by name.
//
// It panics if any of the handler names are not recognized.
Expand Down
55 changes: 55 additions & 0 deletions test_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,4 +281,59 @@ var _ = g.Describe("type Test", func() {
}).To(PanicWith(`the "<app>" application does not have any handlers with names that match the regular expression (^\<proj), or all such handlers have been disabled by a call to ProjectionConfigurer.Disable()`))
})
})

g.Describe("func Annotate()", func() {
g.It("includes annotations in diffs", func() {
app := &Application{
ConfigureFunc: func(c dogma.ApplicationConfigurer) {
c.Identity("<app>", "8ec6465c-d4e3-411c-a05b-898a4b608284")

c.RegisterAggregate(&AggregateMessageHandler{
ConfigureFunc: func(c dogma.AggregateConfigurer) {
c.Identity("<aggregate>", "a9cdc28d-ec85-4130-af86-4a2ae86a43dd")
c.Routes(
dogma.HandlesCommand[MessageC](),
dogma.RecordsEvent[MessageE](),
)
},
RouteCommandToInstanceFunc: func(dogma.Command) string {
return "<instance>"
},
HandleCommandFunc: func(
_ dogma.AggregateRoot,
s dogma.AggregateCommandScope,
m dogma.Command,
) {
s.RecordEvent(MessageE1)
},
})
},
}

t := &testingmock.T{FailSilently: true}

Begin(t, app).
Annotate("E1", "anna's customer ID").
Annotate("E2", "bob's customer ID").
Expect(
ExecuteCommand(MessageC1),
ToRecordEvent(MessageE2),
)

expectReport(
`✗ record a specific 'fixtures.MessageE' event`,
``,
` | EXPLANATION`,
` | a similar event was recorded by the '<aggregate>' aggregate message handler`,
` | `,
` | SUGGESTIONS`,
` | • check the content of the message`,
` | `,
` | MESSAGE DIFF`,
` | fixtures.MessageE{`,
` | Value: "E[-2-]{+1+}" <<[-bob-]{+anna+}'s customer ID>>`,
` | }`,
)(t)
})
})
})

0 comments on commit 90513df

Please sign in to comment.