88 "regexp"
99 "sort"
1010 "strings"
11+ "unicode/utf8"
1112
1213 "github.com/fatih/color"
1314 "github.com/tidwall/gjson"
@@ -19,6 +20,8 @@ type colorRange struct {
1920 End int // End is the ending index of the range.
2021}
2122
23+ const maxCharactersLength = 30
24+
2225// Diff holds the colorized differences between the expected and actual JSON responses.
2326// Expected: The colorized string representing the differences in the expected JSON response.
2427// Actual: The colorized string representing the differences in the actual JSON response.
@@ -61,8 +64,8 @@ func CompareJSON(expectedJSON []byte, actualJSON []byte, noise map[string][]stri
6164 highlightActual := color .FgHiGreen
6265
6366 return Diff {
64- Expected : breakSliceWithColor (expectedJSONString , & highlightExpected , offset ),
65- Actual : breakSliceWithColor (actualJSONString , & highlightActual , offset ),
67+ Expected : truncateStringWithEllipsis ( breakSliceWithColor (expectedJSONString , & highlightExpected , offset ), highlightExpected ),
68+ Actual : truncateStringWithEllipsis ( breakSliceWithColor (actualJSONString , & highlightActual , offset ), highlightActual ),
6669 }, nil
6770 }
6871
@@ -111,8 +114,8 @@ func Compare(expectedJSON, actualJSON string) Diff {
111114 highlightActual := color .FgHiGreen
112115
113116 // Colorize the differences in the expected and actual JSON strings.
114- colorizedExpected := breakSliceWithColor (expectedJSON , & highlightExpected , offsetExpected )
115- colorizedActual := breakSliceWithColor (actualJSON , & highlightActual , offsetActual )
117+ colorizedExpected := truncateStringWithEllipsis ( breakSliceWithColor (expectedJSON , & highlightExpected , offsetExpected ), highlightExpected )
118+ colorizedActual := truncateStringWithEllipsis ( breakSliceWithColor (actualJSON , & highlightActual , offsetActual ), highlightActual )
116119
117120 // Return the colorized differences in a Diff struct.
118121 return Diff {
@@ -247,10 +250,13 @@ func writeKeyValuePair(builder *strings.Builder, key string, value interface{},
247250
248251 builder .WriteString (fmt .Sprintf ("%s\" %s\" : %s,\n " , indent , key , formattedValue ))
249252 default :
250-
251253 serializedValue , _ := json .MarshalIndent (value , "" , " " )
252254 formattedValue := string (serializedValue )
253255
256+ if len (string (serializedValue )) > maxCharactersLength {
257+ formattedValue = string (serializedValue [:maxCharactersLength ]) + "..."
258+ }
259+
254260 // Check if a color function is provided and the value is not empty.
255261 if applyColor != nil && value != "" {
256262 formattedValue = applyColor (formattedValue )
@@ -425,13 +431,13 @@ func compare(key string, val1, val2 interface{}, indent string, expect, actual *
425431 return
426432 }
427433 // Colorize the differences in the values
428- c := color .FgRed
434+ redColor := color .FgRed
429435 offsetsStr1 , offsetsStr2 , _ := diffArrayRange (string (val1Str ), string (val2Str ))
430- expectDiff := breakSliceWithColor (string (val1Str ), & c , offsetsStr1 )
431- c = color .FgGreen
432- actualDiff := breakSliceWithColor (string (val2Str ), & c , offsetsStr2 )
433- expect .WriteString (breakLines (fmt .Sprintf ("%s\" %s\" : %s,\n " , indent , key , string (expectDiff ))))
434- actual .WriteString (breakLines (fmt .Sprintf ("%s\" %s\" : %s,\n " , indent , key , string (actualDiff ))))
436+ expectDiff := breakSliceWithColor (string (val1Str ), & redColor , offsetsStr1 )
437+ greenColor : = color .FgGreen
438+ actualDiff := breakSliceWithColor (string (val2Str ), & greenColor , offsetsStr2 )
439+ expect .WriteString (breakLines (fmt .Sprintf ("%s\" %s\" : %s,\n " , indent , key , truncateStringWithEllipsis ( string (expectDiff ), redColor ))))
440+ actual .WriteString (breakLines (fmt .Sprintf ("%s\" %s\" : %s,\n " , indent , key , truncateStringWithEllipsis ( string (actualDiff ), greenColor ))))
435441 return
436442 }
437443 // If values are equal, write the value without color
@@ -813,6 +819,135 @@ func truncateToMatchWithEllipsis(expectedText, actualText string) (string, strin
813819 return truncatedExpected , truncatedActual
814820}
815821
822+ // truncateStringWithEllipsis truncates a string to a specified length, adding an ellipsis if necessary.
823+ func truncateStringWithEllipsis (val string , c color.Attribute ) string {
824+ if ! ansiRegex .MatchString (val ) {
825+ return truncatePlain (val , maxCharactersLength , "..." )
826+ }
827+
828+ colorEllipsis := color .New (c ).Sprint ("..." )
829+
830+ type ansiRange struct { Start , End int }
831+ ranges := collectANSISegments (val )
832+
833+ // NEW: coalesce adjacent runs (or runs separated only by whitespace)
834+ coalesced := make ([]ansiRange , 0 , len (ranges ))
835+ for _ , r := range ranges {
836+ if len (coalesced ) == 0 {
837+ coalesced = append (coalesced , r )
838+ continue
839+ }
840+ last := & coalesced [len (coalesced )- 1 ]
841+ gap := val [last .End :r .Start ]
842+ if strings .TrimSpace (gap ) == "" {
843+ // merge into one big block (includes the whitespace gap)
844+ last .End = r .End
845+ } else {
846+ coalesced = append (coalesced , r )
847+ }
848+ }
849+
850+ var out strings.Builder
851+ prev := 0
852+ for _ , r := range coalesced {
853+ out .WriteString (truncatePlain (val [prev :r .Start ], maxCharactersLength , "..." ))
854+ out .WriteString (truncateANSISegment (val [r .Start :r .End ], maxCharactersLength , colorEllipsis ))
855+ prev = r .End
856+ }
857+ out .WriteString (truncatePlain (val [prev :], maxCharactersLength , "..." ))
858+ return out .String ()
859+ }
860+
861+ // collectANSISegments collects segments of ANSI escape codes from the input string.
862+ func collectANSISegments (s string ) []struct { Start , End int } {
863+ var res []struct { Start , End int }
864+
865+ in := false
866+ segStart := 0
867+ ms := ansiRegex .FindAllStringIndex (s , - 1 )
868+ for _ , m := range ms {
869+ code := s [m [0 ]:m [1 ]]
870+ if ! in {
871+ if code != ansiResetCode {
872+ in = true
873+ segStart = m [0 ]
874+ }
875+ } else {
876+ if code == ansiResetCode {
877+ res = append (res , struct { Start , End int }{Start : segStart , End : m [1 ]})
878+ in = false
879+ }
880+ }
881+ }
882+ if in { // unclosed color
883+ res = append (res , struct { Start , End int }{Start : segStart , End : len (s )})
884+ }
885+ return res
886+ }
887+
888+ func truncatePlain (s string , limit int , ellipsis string ) string {
889+ if utf8 .RuneCountInString (s ) <= limit {
890+ return s
891+ }
892+ cut := byteIndexAfterNRunes (s , limit )
893+ return s [:cut ] + ellipsis
894+ }
895+
896+ func truncateANSISegment (seg string , limit int , coloredEllipsis string ) string {
897+ hasReset := strings .Contains (seg , ansiResetCode )
898+ ms := ansiRegex .FindAllStringIndex (seg , - 1 )
899+
900+ var b strings.Builder
901+ pos , mi , visible := 0 , 0 , 0
902+
903+ for pos < len (seg ) && visible < limit {
904+ if mi < len (ms ) && pos == ms [mi ][0 ] {
905+ b .WriteString (seg [ms [mi ][0 ]:ms [mi ][1 ]])
906+ pos = ms [mi ][1 ]
907+ mi ++
908+ continue
909+ }
910+ _ , sz := utf8 .DecodeRuneInString (seg [pos :])
911+ b .WriteString (seg [pos : pos + sz ])
912+ pos += sz
913+ visible ++
914+ }
915+
916+ if visible >= limit && moreVisibleAhead (seg , pos , ms , mi ) {
917+ b .WriteString (coloredEllipsis )
918+ }
919+ if hasReset && ! strings .HasSuffix (b .String (), ansiResetCode ) {
920+ b .WriteString (ansiResetCode )
921+ }
922+ return b .String ()
923+ }
924+
925+ func moreVisibleAhead (s string , pos int , ms [][]int , mi int ) bool {
926+ for pos < len (s ) {
927+ if mi < len (ms ) && pos == ms [mi ][0 ] {
928+ pos = ms [mi ][1 ]
929+ mi ++
930+ continue
931+ }
932+ return true
933+ }
934+ return false
935+ }
936+
937+ func byteIndexAfterNRunes (s string , n int ) int {
938+ if n <= 0 {
939+ return 0
940+ }
941+ count := 0
942+ for i := range s {
943+ if count == n {
944+ return i
945+ }
946+ count ++
947+ }
948+ return len (s )
949+ }
950+
816951// compareAndColorizeMaps compares two maps and returns the differences as colorized strings.
817952// a: The first map to compare.
818953// b: The second map to compare.
0 commit comments