Skip to content

Commit

Permalink
Add format.GomegaStringer (#427)
Browse files Browse the repository at this point in the history
* Add format.GomegaString() to allow custom formatting of objects for tests only

* Truncate long object string representations
  • Loading branch information
khaf authored Mar 18, 2021
1 parent c3c0920 commit cc80b6f
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 12 deletions.
71 changes: 59 additions & 12 deletions format/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import (
// Use MaxDepth to set the maximum recursion depth when printing deeply nested objects
var MaxDepth = uint(10)

// MaxLength of the string representation of an object.
// If MaxLength is set to 0, the Object will not be truncted.
var MaxLength = 4000

/*
By default, all objects (even those that implement fmt.Stringer and fmt.GoStringer) are recursively inspected to generate output.
Expand Down Expand Up @@ -53,6 +57,14 @@ var Indent = " "

var longFormThreshold = 20

// GomegaStringer allows for custom formating of objects for gomega.
type GomegaStringer interface {
// GomegaString will be used to custom format an object.
// It does not follow UseStringerRepresentation value and will always be called regardless.
// It also ignores the MaxLength value.
GomegaString() string
}

/*
Generates a formatted matcher success/failure message of the form:
Expand Down Expand Up @@ -159,6 +171,34 @@ func findFirstMismatch(a, b string) int {
return 0
}

const truncateHelpText = `the very very long object here that goes on forever and ever but then gets trunc....
Gomega truncated this representation as it exceeds 'format.MaxLength'.
Consider having the object provide a custom 'GomegaStringer' representation
or adjust the parameters in Gomega's 'format' package.
Learn more here: https://onsi.github.io/gomega/#adjusting-output
`

func truncateLongStrings(s string) string {
if MaxLength > 0 && len(s) > MaxLength {
var sb strings.Builder
for i, r := range s {
if i < MaxLength {
sb.WriteRune(r)
continue
}
break
}

sb.WriteString("\n")
sb.WriteString(truncateHelpText)

return sb.String()
}
return s
}

/*
Pretty prints the passed in object at the passed in indentation level.
Expand Down Expand Up @@ -219,14 +259,21 @@ func formatValue(value reflect.Value, indentation uint) string {
return "nil"
}

if UseStringerRepresentation {
if value.CanInterface() {
obj := value.Interface()
if value.CanInterface() {
obj := value.Interface()

// GomegaStringer will take precedence to other representations and disregards UseStringerRepresentation
if x, ok := obj.(GomegaStringer); ok {
// do not truncate a user-defined GoMegaString() value
return x.GomegaString()
}

if UseStringerRepresentation {
switch x := obj.(type) {
case fmt.GoStringer:
return x.GoString()
return truncateLongStrings(x.GoString())
case fmt.Stringer:
return x.String()
return truncateLongStrings(x.String())
}
}
}
Expand Down Expand Up @@ -257,26 +304,26 @@ func formatValue(value reflect.Value, indentation uint) string {
case reflect.Ptr:
return formatValue(value.Elem(), indentation)
case reflect.Slice:
return formatSlice(value, indentation)
return truncateLongStrings(formatSlice(value, indentation))
case reflect.String:
return formatString(value.String(), indentation)
return truncateLongStrings(formatString(value.String(), indentation))
case reflect.Array:
return formatSlice(value, indentation)
return truncateLongStrings(formatSlice(value, indentation))
case reflect.Map:
return formatMap(value, indentation)
return truncateLongStrings(formatMap(value, indentation))
case reflect.Struct:
if value.Type() == timeType && value.CanInterface() {
t, _ := value.Interface().(time.Time)
return t.Format(time.RFC3339Nano)
}
return formatStruct(value, indentation)
return truncateLongStrings(formatStruct(value, indentation))
case reflect.Interface:
return formatInterface(value, indentation)
default:
if value.CanInterface() {
return fmt.Sprintf("%#v", value.Interface())
return truncateLongStrings(fmt.Sprintf("%#v", value.Interface()))
}
return fmt.Sprintf("%#v", value)
return truncateLongStrings(fmt.Sprintf("%#v", value))
}
}

Expand Down
44 changes: 44 additions & 0 deletions format/format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,20 @@ func (g Stringer) String() string {
return "string"
}

type gomegaStringer struct {
}

func (g gomegaStringer) GomegaString() string {
return "gomegastring"
}

type gomegaStringerLong struct {
}

func (g gomegaStringerLong) GomegaString() string {
return strings.Repeat("s", MaxLength*2)
}

var _ = Describe("Format", func() {
match := func(typeRepresentation string, valueRepresentation string, args ...interface{}) types.GomegaMatcher {
if len(args) > 0 {
Expand All @@ -100,9 +114,25 @@ var _ = Describe("Format", func() {

Describe("Message", func() {
Context("with only an actual value", func() {
BeforeEach(func() {
MaxLength = 4000
})

It("should print out an indented formatted representation of the value and the message", func() {
Expect(Message(3, "to be three.")).Should(Equal("Expected\n <int>: 3\nto be three."))
})

It("should print out an indented formatted representation of the value and the message, and trucate it when too long", func() {
tooLong := strings.Repeat("s", MaxLength+1)
tooLongResult := strings.Repeat("s", MaxLength) + "\n" + TruncatedHelpText()
Expect(Message(tooLong, "to be truncated")).Should(Equal("Expected\n <string>: " + tooLongResult + "\nto be truncated"))
})

It("should print out an indented formatted representation of the value and the message, and not trucate it when MaxLength = 0", func() {
MaxLength = 0
tooLong := strings.Repeat("s", MaxLength+1)
Expect(Message(tooLong, "to be truncated")).Should(Equal("Expected\n <string>: " + tooLong + "\nto be truncated"))
})
})

Context("with an actual and an expected value", func() {
Expand Down Expand Up @@ -654,6 +684,20 @@ var _ = Describe("Format", func() {
Expect(Object(Stringer{}, 1)).Should(ContainSubstring("<format_test.Stringer>: string"))
})
})

When("passed a GomegaStringer", func() {
It("should use what GomegaString() returns", func() {
Expect(Object(gomegaStringer{}, 1)).Should(ContainSubstring("<format_test.gomegaStringer>: gomegastring"))
UseStringerRepresentation = false
Expect(Object(gomegaStringer{}, 1)).Should(ContainSubstring("<format_test.gomegaStringer>: gomegastring"))
})

It("should use what GomegaString() returns, disregarding MaxLength", func() {
Expect(Object(gomegaStringerLong{}, 1)).Should(Equal(" <format_test.gomegaStringerLong>: " + strings.Repeat("s", MaxLength*2)))
UseStringerRepresentation = false
Expect(Object(gomegaStringerLong{}, 1)).Should(Equal(" <format_test.gomegaStringerLong>: " + strings.Repeat("s", MaxLength*2)))
})
})
})

Describe("Printing a context.Context field", func() {
Expand Down
11 changes: 11 additions & 0 deletions format/helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
Gomega's format test helper package.
*/

package format

// TruncateHelpText returns truncateHelpText.
// This function is only accessible during tests.
func TruncatedHelpText() string {
return truncateHelpText
}

0 comments on commit cc80b6f

Please sign in to comment.