diff --git a/format/format.go b/format/format.go index 4f7462ab1..75f9be1a3 100644 --- a/format/format.go +++ b/format/format.go @@ -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. @@ -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: @@ -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. @@ -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()) } } } @@ -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)) } } diff --git a/format/format_test.go b/format/format_test.go index 8f20767e8..1c0bf4d74 100644 --- a/format/format_test.go +++ b/format/format_test.go @@ -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 { @@ -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 : 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 : " + 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 : " + tooLong + "\nto be truncated")) + }) }) Context("with an actual and an expected value", func() { @@ -654,6 +684,20 @@ var _ = Describe("Format", func() { Expect(Object(Stringer{}, 1)).Should(ContainSubstring(": string")) }) }) + + When("passed a GomegaStringer", func() { + It("should use what GomegaString() returns", func() { + Expect(Object(gomegaStringer{}, 1)).Should(ContainSubstring(": gomegastring")) + UseStringerRepresentation = false + Expect(Object(gomegaStringer{}, 1)).Should(ContainSubstring(": gomegastring")) + }) + + It("should use what GomegaString() returns, disregarding MaxLength", func() { + Expect(Object(gomegaStringerLong{}, 1)).Should(Equal(" : " + strings.Repeat("s", MaxLength*2))) + UseStringerRepresentation = false + Expect(Object(gomegaStringerLong{}, 1)).Should(Equal(" : " + strings.Repeat("s", MaxLength*2))) + }) + }) }) Describe("Printing a context.Context field", func() { diff --git a/format/helper_test.go b/format/helper_test.go new file mode 100644 index 000000000..189f3d901 --- /dev/null +++ b/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 +}