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 format.GomegaStringer #427

Merged
merged 2 commits into from Mar 18, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
71 changes: 59 additions & 12 deletions format/format.go
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
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
@@ -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
}