From ae27338d8f6606f21c648ed82c107d2194493f3a Mon Sep 17 00:00:00 2001 From: Aleksandr Matsko <90016901+AleksandrMatsko@users.noreply.github.com> Date: Wed, 23 Oct 2024 19:45:26 +0700 Subject: [PATCH] refactor: change alert parts calculations (#1084) --- senders/calc_message_parts.go | 76 +++++++- senders/calc_message_parts_test.go | 177 +++++++++++++++++ senders/msgformat/defaults.go | 42 +++++ senders/msgformat/defaults_test.go | 43 +++++ senders/msgformat/highlighter.go | 40 ++-- senders/msgformat/highlighter_test.go | 209 +++++++++++++++++---- senders/msgformat/msgformat.go | 7 - senders/slack/slack_test.go | 34 +++- senders/telegram/message_formatter.go | 50 +++-- senders/telegram/message_formatter_test.go | 197 ++++++++++++++----- 10 files changed, 738 insertions(+), 137 deletions(-) create mode 100644 senders/msgformat/defaults.go create mode 100644 senders/msgformat/defaults_test.go diff --git a/senders/calc_message_parts.go b/senders/calc_message_parts.go index bebb2c928..ee4aeaf2a 100644 --- a/senders/calc_message_parts.go +++ b/senders/calc_message_parts.go @@ -6,11 +6,81 @@ func CalculateMessagePartsLength(maxChars, descLen, eventsLen int) (descNewLen i if descLen+eventsLen <= maxChars { return descLen, eventsLen } - if descLen > maxChars/2 && eventsLen <= maxChars/2 { + + halfOfMaxChars := maxChars / partsCountForMessageWithDescAndEvents + + if descLen > halfOfMaxChars && eventsLen <= halfOfMaxChars { return maxChars - eventsLen - 10, eventsLen } - if eventsLen > maxChars/2 && descLen <= maxChars/2 { + + if eventsLen > halfOfMaxChars && descLen <= halfOfMaxChars { return descLen, maxChars - descLen } - return maxChars/2 - 10, maxChars / 2 + + return halfOfMaxChars - 10, halfOfMaxChars +} + +const ( + // partsCountForMessageWithDescAndEvents is used then you need to split given maxChars fairly by half + // between description and events. + partsCountForMessageWithDescAndEvents = 2 + // partsCountForMessageWithTagsDescAndEvents is used then you need to split given maxChars fairly by three parts + // between tags, description and events. + partsCountForMessageWithTagsDescAndEvents = 3 +) + +// CalculateMessagePartsBetweenTagsDescEvents calculates and returns the length of tags, description and events string +// in order to fit the max chars limit. +func CalculateMessagePartsBetweenTagsDescEvents(maxChars, tagsLen, descLen, eventsLen int) (tagsNewLen int, descNewLen int, eventsNewLen int) { // nolint + if maxChars <= 0 { + return 0, 0, 0 + } + + if tagsLen+descLen+eventsLen <= maxChars { + return tagsLen, descLen, eventsLen + } + + fairMaxLen := maxChars / partsCountForMessageWithTagsDescAndEvents + + switch { + case tagsLen > fairMaxLen && descLen <= fairMaxLen && eventsLen <= fairMaxLen: + // give free space to tags + tagsNewLen = maxChars - descLen - eventsLen + + return min(tagsNewLen, tagsLen), descLen, eventsLen + case tagsLen <= fairMaxLen && descLen > fairMaxLen && eventsLen <= fairMaxLen: + // give free space to description + descNewLen = maxChars - tagsLen - eventsLen + + return tagsLen, min(descNewLen, descLen), eventsLen + case tagsLen <= fairMaxLen && descLen <= fairMaxLen && eventsLen > fairMaxLen: + // give free space to events + eventsNewLen = maxChars - tagsLen - descLen + + return tagsLen, descLen, min(eventsNewLen, eventsLen) + case tagsLen > fairMaxLen && descLen > fairMaxLen && eventsLen <= fairMaxLen: + // description is more important than tags + tagsNewLen = fairMaxLen + descNewLen = maxChars - tagsNewLen - eventsLen + + return tagsNewLen, min(descNewLen, descLen), eventsLen + case tagsLen > fairMaxLen && descLen <= fairMaxLen && eventsLen > fairMaxLen: + // events are more important than tags + tagsNewLen = fairMaxLen + eventsNewLen = maxChars - tagsNewLen - descLen + + return tagsNewLen, descLen, min(eventsNewLen, eventsLen) + case tagsLen <= fairMaxLen && descLen > fairMaxLen && eventsLen > fairMaxLen: + // split free space from tags fairly between description and events + spaceFromTags := fairMaxLen - tagsLen + halfOfSpaceFromTags := spaceFromTags / partsCountForMessageWithDescAndEvents + + descNewLen = fairMaxLen + halfOfSpaceFromTags + eventsNewLen = fairMaxLen + halfOfSpaceFromTags + + return tagsLen, min(descNewLen, descLen), min(eventsNewLen, eventsLen) + default: + // all 3 blocks have length greater than maxChars/3, so split space fairly + return fairMaxLen, fairMaxLen, fairMaxLen + } } diff --git a/senders/calc_message_parts_test.go b/senders/calc_message_parts_test.go index 19882a9bd..82a17d60d 100644 --- a/senders/calc_message_parts_test.go +++ b/senders/calc_message_parts_test.go @@ -1,6 +1,7 @@ package senders import ( + "fmt" "testing" . "github.com/smartystreets/goconvey/convey" @@ -33,3 +34,179 @@ func TestCalculateMessagePartsLength(t *testing.T) { }) }) } + +func TestCalculateMessagePartsBetweenTagsDescEvents(t *testing.T) { + Convey("Message parts calculating test (for tags, desc, events)", t, func() { + type given struct { + maxChars int + tagsLen int + descLen int + eventsLen int + } + + type expected struct { + tagsLen int + descLen int + eventsLen int + } + + type testcase struct { + given given + expected expected + description string + } + + cases := []testcase{ + { + description: "with maxChars < 0", + given: given{ + maxChars: -1, + tagsLen: 10, + descLen: 10, + eventsLen: 10, + }, + expected: expected{ + tagsLen: 0, + descLen: 0, + eventsLen: 0, + }, + }, + { + description: "with tagsLen + descLen + eventsLen <= maxChars", + given: given{ + maxChars: 100, + tagsLen: 20, + descLen: 50, + eventsLen: 30, + }, + expected: expected{ + tagsLen: 20, + descLen: 50, + eventsLen: 30, + }, + }, + { + description: "with tagsLen > maxChars/3, descLen and eventsLen <= maxChars/3", + given: given{ + maxChars: 100, + tagsLen: 50, + descLen: 30, + eventsLen: 30, + }, + expected: expected{ + tagsLen: 40, + descLen: 30, + eventsLen: 30, + }, + }, + { + description: "with descLen > maxChars/3, tagsLen and eventsLen <= maxChars/3", + given: given{ + maxChars: 100, + tagsLen: 30, + descLen: 50, + eventsLen: 31, + }, + expected: expected{ + tagsLen: 30, + descLen: 39, + eventsLen: 31, + }, + }, + { + description: "with eventsLen > maxChars/3, tagsLen and descLen <= maxChars/3", + given: given{ + maxChars: 100, + tagsLen: 33, + descLen: 33, + eventsLen: 61, + }, + expected: expected{ + tagsLen: 33, + descLen: 33, + eventsLen: 34, + }, + }, + { + description: "with tagsLen and descLen > maxChars/3, eventsLen <= maxChars/3", + given: given{ + maxChars: 100, + tagsLen: 55, + descLen: 46, + eventsLen: 31, + }, + expected: expected{ + tagsLen: 33, + descLen: 36, + eventsLen: 31, + }, + }, + { + description: "with tagsLen and eventsLen > maxChars/3, descLen <= maxChars/3", + given: given{ + maxChars: 100, + tagsLen: 55, + descLen: 33, + eventsLen: 100, + }, + expected: expected{ + tagsLen: 33, + descLen: 33, + eventsLen: 34, + }, + }, + { + description: "with descLen and eventsLen > maxChars/3, tagsLen <= maxChars/3", + given: given{ + maxChars: 100, + tagsLen: 29, + descLen: 56, + eventsLen: 100, + }, + expected: expected{ + tagsLen: 29, + descLen: 35, + eventsLen: 35, + }, + }, + { + description: "with tagsLen, descLen and eventsLen > maxChars/3", + given: given{ + maxChars: 100, + tagsLen: 55, + descLen: 40, + eventsLen: 100, + }, + expected: expected{ + tagsLen: 33, + descLen: 33, + eventsLen: 33, + }, + }, + { + description: "with tagsLen, descLen > maxChars/3, eventsLen <= maxChars/3 and maxChars - maxChars/3 - eventsLen > descLen", + given: given{ + maxChars: 100, + tagsLen: 100, + descLen: 34, + eventsLen: 20, + }, + expected: expected{ + tagsLen: 33, + descLen: 34, + eventsLen: 20, + }, + }, + } + + for i, c := range cases { + Convey(fmt.Sprintf("case %d: %s", i+1, c.description), func() { + tagsNewLen, descNewLen, eventsNewLen := CalculateMessagePartsBetweenTagsDescEvents(c.given.maxChars, c.given.tagsLen, c.given.descLen, c.given.eventsLen) + + So(tagsNewLen, ShouldResemble, c.expected.tagsLen) + So(descNewLen, ShouldResemble, c.expected.descLen) + So(eventsNewLen, ShouldResemble, c.expected.eventsLen) + }) + } + }) +} diff --git a/senders/msgformat/defaults.go b/senders/msgformat/defaults.go new file mode 100644 index 000000000..ad485a348 --- /dev/null +++ b/senders/msgformat/defaults.go @@ -0,0 +1,42 @@ +package msgformat + +import "unicode/utf8" + +// DefaultDescriptionCutter cuts description, so len(newDesc) <= maxSize. Ensure that len(desc) >= maxSize and +// maxSize >= len("...\n"). +func DefaultDescriptionCutter(desc string, maxSize int) string { + suffix := "...\n" + return desc[:maxSize-len(suffix)] + suffix +} + +var bracketsLen = utf8.RuneCountInString("[]") + +// DefaultTagsLimiter cuts and formats tags to fit maxSize. There will be no tag parts, for example: +// +// if we have +// +// tags = []string{"tag1", "tag2} +// maxSize = 8 +// +// so call DefaultTagsLimiter(tags, maxSize) will return " [tag1]". +func DefaultTagsLimiter(tags []string, maxSize int) string { + tagsStr := " " + lenTagsStr := utf8.RuneCountInString(tagsStr) + + for i := range tags { + lenTag := utf8.RuneCountInString(tags[i]) + bracketsLen + + if lenTagsStr+lenTag > maxSize { + break + } + + tagsStr += "[" + tags[i] + "]" + lenTagsStr += lenTag + } + + if tagsStr == " " { + return "" + } + + return tagsStr +} diff --git a/senders/msgformat/defaults_test.go b/senders/msgformat/defaults_test.go new file mode 100644 index 000000000..2704e905f --- /dev/null +++ b/senders/msgformat/defaults_test.go @@ -0,0 +1,43 @@ +package msgformat + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestDefaultTagsLimiter(t *testing.T) { + Convey("Test default tags limiter", t, func() { + tags := []string{"tag1", "tag2"} + + Convey("with maxSize < 0", func() { + tagsStr := DefaultTagsLimiter(tags, -1) + + So(tagsStr, ShouldResemble, "") + }) + + Convey("with maxSize > total characters in tags string", func() { + tagsStr := DefaultTagsLimiter(tags, 30) + + So(tagsStr, ShouldResemble, " [tag1][tag2]") + }) + + Convey("with maxSize not enough for all tags", func() { + tagsStr := DefaultTagsLimiter(tags, 8) + + So(tagsStr, ShouldResemble, " [tag1]") + }) + + Convey("with one long tag > maxSize", func() { + tagsStr := DefaultTagsLimiter([]string{"long_tag"}, 4) + + So(tagsStr, ShouldResemble, "") + }) + + Convey("with no tags", func() { + tagsStr := DefaultTagsLimiter([]string{}, 0) + + So(tagsStr, ShouldResemble, "") + }) + }) +} diff --git a/senders/msgformat/highlighter.go b/senders/msgformat/highlighter.go index 9a78403e4..2e21387fe 100644 --- a/senders/msgformat/highlighter.go +++ b/senders/msgformat/highlighter.go @@ -2,7 +2,6 @@ package msgformat import ( "fmt" - "strings" "time" "unicode/utf8" @@ -27,6 +26,10 @@ type EventStringFormatter func(event moira.NotificationEvent, location *time.Loc // DescriptionCutter cuts the given description to fit max size. type DescriptionCutter func(desc string, maxSize int) string +// TagsLimiter should prepare tags string in format like " [tag1][tag2][tag3]", +// but characters count should be less than or equal to maxSize. +type TagsLimiter func(tags []string, maxSize int) string + // highlightSyntaxFormatter formats message by using functions, emojis and some other highlight patterns. type highlightSyntaxFormatter struct { // emojiGetter used in titles for better description. @@ -74,35 +77,44 @@ func NewHighlightSyntaxFormatter( // Format formats message using given params and formatter functions. func (formatter *highlightSyntaxFormatter) Format(params MessageFormatterParams) string { - var message strings.Builder state := params.Events.GetCurrentState(params.Throttled) emoji := formatter.emojiGetter.GetStateEmoji(state) title := formatter.buildTitle(params.Events, params.Trigger, emoji, params.Throttled) - titleLen := utf8.RuneCountInString(title) + titleLen := utf8.RuneCountInString(title) + len("\n") + + var tags string + var tagsLen int + + triggerTags := params.Trigger.GetTags() + if len(triggerTags) != 0 { + tags = " " + triggerTags + tagsLen = utf8.RuneCountInString(tags) + } desc := formatter.descriptionFormatter(params.Trigger) descLen := utf8.RuneCountInString(desc) - eventsString := formatter.buildEventsString(params.Events, -1, params.Throttled) - eventsStringLen := utf8.RuneCountInString(eventsString) + events := formatter.buildEventsString(params.Events, -1, params.Throttled) + eventsStringLen := utf8.RuneCountInString(events) charsLeftAfterTitle := params.MessageMaxChars - titleLen - descNewLen, eventsNewLen := senders.CalculateMessagePartsLength(charsLeftAfterTitle, descLen, eventsStringLen) + tagsNewLen, descNewLen, eventsNewLen := senders.CalculateMessagePartsBetweenTagsDescEvents(charsLeftAfterTitle, tagsLen, descLen, eventsStringLen) + if tagsNewLen != tagsLen { + tags = DefaultTagsLimiter(params.Trigger.Tags, tagsNewLen) + } if descLen != descNewLen { desc = formatter.descriptionCutter(desc, descNewLen) } if eventsNewLen != eventsStringLen { - eventsString = formatter.buildEventsString(params.Events, eventsNewLen, params.Throttled) + events = formatter.buildEventsString(params.Events, eventsNewLen, params.Throttled) } - message.WriteString(title) - message.WriteString(desc) - message.WriteString(eventsString) - return message.String() + return title + tags + "\n" + desc + events } +// buildTitle builds title string for alert (emoji, trigger state, trigger name with link). func (formatter *highlightSyntaxFormatter) buildTitle(events moira.NotificationEvents, trigger moira.TriggerData, emoji string, throttled bool) string { state := events.GetCurrentState(throttled) title := "" @@ -118,12 +130,6 @@ func (formatter *highlightSyntaxFormatter) buildTitle(events moira.NotificationE title += " " + trigger.Name } - tags := trigger.GetTags() - if tags != "" { - title += " " + tags - } - - title += "\n" return title } diff --git a/senders/msgformat/highlighter_test.go b/senders/msgformat/highlighter_test.go index 470db230e..0390e0802 100644 --- a/senders/msgformat/highlighter_test.go +++ b/senders/msgformat/highlighter_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" "time" + "unicode/utf8" "github.com/moira-alert/moira" "github.com/moira-alert/moira/senders/emoji_provider" @@ -89,64 +90,196 @@ func TestFormat(t *testing.T) { }) Convey("Long message parts", func() { + trigger.Desc = "" + trigger.Tags = []string{} + const ( - msgLimit = 4_000 - halfLimit = msgLimit / 2 - greaterThanHalf = halfLimit + 100 - lessThanHalf = halfLimit - 100 + titleLine = "**NODATA** [Name](http://moira.url/trigger/TriggerID)" + eventLine = "\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)" + endSuffix = "...\n" + lenEndSuffix = 4 ) - const eventLine = "\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)" - oneEventLineLen := len([]rune(eventLine)) - - longDesc := strings.Repeat("a", greaterThanHalf) + lenTitle := utf8.RuneCountInString(titleLine) + len("\n") // 54 symbols + oneEventLineLen := utf8.RuneCountInString(eventLine) // 47 symbols - // Events list with chars greater than half of the message limit - var longEvents moira.NotificationEvents - for i := 0; i < greaterThanHalf/oneEventLineLen; i++ { - longEvents = append(longEvents, event) - } + var ( + msgLimit = testMaxChars - lenTitle // 3947 + thirdOfLimit = msgLimit / 3 // 1315 + greaterThanThird = thirdOfLimit + 100 // 1415 + lessThanThird = thirdOfLimit - 100 // 1215 + ) - Convey("Long description. desc > msgLimit/2", func() { - var events moira.NotificationEvents - for i := 0; i < lessThanHalf/oneEventLineLen; i++ { - events = append(events, event) + Convey("with long tags (tagsLen >= msgLimit), desc and events < msgLimit/3", func() { + trigger.Tags = []string{ + strings.Repeat("a", 1000), + strings.Repeat("b", 1000), + strings.Repeat("c", 1000), + strings.Repeat("d", 1000), } + trigger.Desc = genDescByLimit(lessThanThird) + events := genEventsByLimit(event, oneEventLineLen, lessThanThird) + + expected := titleLine + + DefaultTagsLimiter(trigger.Tags, + msgLimit-utf8.RuneCountInString(trigger.Desc)-len("```\n```")-oneEventLineLen*len(events), + ) + "\n" + + strings.Repeat("a", lessThanThird) + "\n" + + "```" + + strings.Repeat(eventLine, len(events)) + + "\n```" + + actual := formatter.Format(getParams(events, trigger, false)) - actual := formatter.Format(getParams(events, moira.TriggerData{Desc: longDesc}, false)) - expected := "**NODATA**\n" + - strings.Repeat("a", 2100) + "\n" + - "```\n" + - strings.Repeat("02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n", 39) + - "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```" So(actual, ShouldResemble, expected) + So(utf8.RuneCountInString(actual), ShouldBeLessThanOrEqualTo, testMaxChars) }) - Convey("Many events. eventString > msgLimit/2", func() { - desc := strings.Repeat("a", lessThanHalf) - actual := formatter.Format(getParams(longEvents, moira.TriggerData{Desc: desc}, false)) - expected := "**NODATA**\n" + - desc + "\n" + - "```\n" + - strings.Repeat("02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n", 43) + - "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```" + Convey("with description > msgLimit/3, tags and events < msgLimit/3, and sum of lengths is greater than msgLimit", func() { + longDescLen := greaterThanThird + 200 + + trigger.Tags = genTagsByLimit(lessThanThird) + trigger.Desc = genDescByLimit(longDescLen) + events := genEventsByLimit(event, oneEventLineLen, lessThanThird) + + tagsStr := " " + trigger.GetTags() + + expected := titleLine + tagsStr + "\n" + + strings.Repeat("a", + msgLimit-utf8.RuneCountInString(tagsStr)-len("```\n```")-oneEventLineLen*len(events)-lenEndSuffix, + ) + endSuffix + + "```" + + strings.Repeat(eventLine, len(events)) + + "\n```" + + actual := formatter.Format(getParams(events, trigger, false)) + So(actual, ShouldResemble, expected) + So(utf8.RuneCountInString(actual), ShouldBeLessThanOrEqualTo, testMaxChars) }) - Convey("Long description and many events. both desc and events > msgLimit/2", func() { - actual := formatter.Format(getParams(longEvents, moira.TriggerData{Desc: longDesc}, false)) - expected := "**NODATA**\n" + - strings.Repeat("a", 1980) + "...\n" + - "```\n" + - strings.Repeat("02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n", 40) + - "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```\n" + + Convey("with long events string (> msgLimit/3), desc and tags < msgLimit/3", func() { + longEventsLen := greaterThanThird + 200 + + trigger.Tags = genTagsByLimit(lessThanThird) + trigger.Desc = genDescByLimit(lessThanThird) + events := genEventsByLimit(event, oneEventLineLen, longEventsLen) + + tagsStr := " " + trigger.GetTags() + + expected := titleLine + tagsStr + "\n" + + strings.Repeat("a", lessThanThird) + "\n" + + "```" + + strings.Repeat(eventLine, 31) + + "\n```\n" + "...and 3 more events." + + actual := formatter.Format(getParams(events, trigger, false)) + + So(actual, ShouldResemble, expected) + So(utf8.RuneCountInString(actual), ShouldBeLessThanOrEqualTo, testMaxChars) + }) + + Convey("with tags and desc > msgLimit/3, events <= msgLimit/3", func() { + trigger.Tags = genTagsByLimit(greaterThanThird) + trigger.Desc = genDescByLimit(greaterThanThird) + events := genEventsByLimit(event, oneEventLineLen, lessThanThird) + + expected := titleLine + DefaultTagsLimiter(trigger.Tags, thirdOfLimit) + "\n" + + strings.Repeat("a", greaterThanThird) + "\n" + + "```" + + strings.Repeat(eventLine, len(events)) + + "\n```" + + actual := formatter.Format(getParams(events, trigger, false)) + + So(actual, ShouldResemble, expected) + So(utf8.RuneCountInString(actual), ShouldBeLessThanOrEqualTo, testMaxChars) + }) + + Convey("with tags and events > msgLimit/3, desc <= msgLimit/3", func() { + trigger.Tags = genTagsByLimit(greaterThanThird) + trigger.Desc = genDescByLimit(lessThanThird) + events := genEventsByLimit(event, oneEventLineLen, greaterThanThird) + + expected := titleLine + DefaultTagsLimiter(trigger.Tags, thirdOfLimit) + "\n" + + strings.Repeat("a", lessThanThird) + "\n" + + "```" + + strings.Repeat(eventLine, 29) + + "\n```\n" + "...and 1 more events." + + actual := formatter.Format(getParams(events, trigger, false)) + So(actual, ShouldResemble, expected) + So(utf8.RuneCountInString(actual), ShouldBeLessThanOrEqualTo, testMaxChars) + }) + + Convey("with desc and events > msgLimit/3, tags <= msgLimit/3", func() { + trigger.Tags = genTagsByLimit(lessThanThird) + trigger.Desc = genDescByLimit(greaterThanThird) + events := genEventsByLimit(event, oneEventLineLen, greaterThanThird) + + tagsStr := DefaultTagsLimiter(trigger.Tags, lessThanThird) + + expected := titleLine + tagsStr + "\n" + + strings.Repeat("a", thirdOfLimit+(thirdOfLimit-utf8.RuneCountInString(tagsStr))/2-lenEndSuffix) + endSuffix + + "```" + + strings.Repeat(eventLine, 28) + + "\n```\n" + "...and 2 more events." + + actual := formatter.Format(getParams(events, trigger, false)) + + So(actual, ShouldResemble, expected) + So(utf8.RuneCountInString(actual), ShouldBeLessThanOrEqualTo, testMaxChars) + }) + + Convey("tags, description and events all have len > msgLimit/3", func() { + trigger.Tags = genTagsByLimit(greaterThanThird) + trigger.Desc = genDescByLimit(greaterThanThird) + events := genEventsByLimit(event, oneEventLineLen, greaterThanThird) + + expected := titleLine + DefaultTagsLimiter(trigger.Tags, thirdOfLimit) + "\n" + + strings.Repeat("a", thirdOfLimit-lenEndSuffix) + endSuffix + + "```" + + strings.Repeat(eventLine, thirdOfLimit/oneEventLineLen) + + "\n```\n" + + "...and 3 more events." + + actual := formatter.Format(getParams(events, trigger, false)) + + So(actual, ShouldResemble, expected) + So(utf8.RuneCountInString(actual), ShouldBeLessThanOrEqualTo, testMaxChars) }) }) }) } +func genTagsByLimit(limit int) []string { + tagName := "tag1" + + tagsCount := (limit - 1) / (len(tagName) + 2) + + tags := make([]string, 0, tagsCount) + + for i := 0; i < tagsCount; i++ { + tags = append(tags, tagName) + } + + return tags +} + +func genDescByLimit(limit int) string { + return strings.Repeat("a", limit) +} + +func genEventsByLimit(event moira.NotificationEvent, oneEventLineLen int, limit int) moira.NotificationEvents { + var events moira.NotificationEvents + for i := 0; i < limit/oneEventLineLen; i++ { + events = append(events, event) + } + return events +} + func testBoldFormatter(str string) string { return fmt.Sprintf("**%s**", str) } diff --git a/senders/msgformat/msgformat.go b/senders/msgformat/msgformat.go index 72a6cdb37..42b4aaf59 100644 --- a/senders/msgformat/msgformat.go +++ b/senders/msgformat/msgformat.go @@ -21,10 +21,3 @@ type MessageFormatterParams struct { MessageMaxChars int Throttled bool } - -// DefaultDescriptionCutter cuts description, so len(newDesc) <= maxSize. Ensure that len(desc) >= maxSize and -// maxSize >= len("...\n"). -func DefaultDescriptionCutter(desc string, maxSize int) string { - suffix := "...\n" - return desc[:maxSize-len(suffix)] + suffix -} diff --git a/senders/slack/slack_test.go b/senders/slack/slack_test.go index 37cd85eda..b4049562d 100644 --- a/senders/slack/slack_test.go +++ b/senders/slack/slack_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" "time" + "unicode/utf8" "github.com/go-playground/validator/v10" "github.com/moira-alert/moira" @@ -129,7 +130,8 @@ some other text italic text }) eventLine := "\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)" - oneEventLineLen := len([]rune(eventLine)) + oneEventLineLen := utf8.RuneCountInString(eventLine) + // Events list with chars less than half the message limit var shortEvents moira.NotificationEvents var shortEventsString string @@ -137,6 +139,7 @@ some other text italic text shortEvents = append(shortEvents, event) shortEventsString += eventLine } + // Events list with chars greater than half the message limit var longEvents moira.NotificationEvents var longEventsString string @@ -144,6 +147,7 @@ some other text italic text longEvents = append(longEvents, event) longEventsString += eventLine } + longDesc := strings.Repeat("a", messageMaxCharacters/2+100) Convey("Print moira message with desc + events < msgLimit", func() { @@ -159,22 +163,44 @@ some other text italic text events = append(events, event) eventsString += eventLine } + + expected := "*NODATA*\n" + + strings.Repeat("a", 1991) + "...\n" + + "```" + + eventsString + "\n```" + actual := sender.buildMessage(events, moira.TriggerData{Desc: longDesc}, false) - expected := "*NODATA*\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```" + So(actual, ShouldResemble, expected) + So(utf8.RuneCountInString(actual), ShouldBeLessThanOrEqualTo, messageMaxCharacters) }) Convey("Print moira message events string > msgLimit/2", func() { desc := strings.Repeat("a", messageMaxCharacters/2-100) + + expected := "*NODATA*\n" + + desc + "\n" + + "```" + + strings.Repeat(eventLine, 41) + "\n```" + + "\n...and 5 more events." + actual := sender.buildMessage(longEvents, moira.TriggerData{Desc: desc}, false) - expected := "*NODATA*\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```\n...and 3 more events." + So(actual, ShouldResemble, expected) + So(utf8.RuneCountInString(actual), ShouldBeLessThanOrEqualTo, messageMaxCharacters) }) Convey("Print moira message with both desc and events > msgLimit/2", func() { + expected := "*NODATA*\n" + + strings.Repeat("a", 1991) + "...\n" + + "```" + + strings.Repeat(eventLine, 41) + "\n```" + + "\n...and 5 more events." + actual := sender.buildMessage(longEvents, moira.TriggerData{Desc: longDesc}, false) - expected := "*NODATA*\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```\n...and 5 more events." + So(actual, ShouldResemble, expected) + So(utf8.RuneCountInString(actual), ShouldBeLessThanOrEqualTo, messageMaxCharacters) }) }) } diff --git a/senders/telegram/message_formatter.go b/senders/telegram/message_formatter.go index 8059a2231..a453ceaa8 100644 --- a/senders/telegram/message_formatter.go +++ b/senders/telegram/message_formatter.go @@ -46,33 +46,57 @@ func NewTelegramMessageFormatter( // Format formats message using given params and formatter functions. func (formatter *messageFormatter) Format(params msgformat.MessageFormatterParams) string { + params.Trigger.Tags = htmlEscapeTags(params.Trigger.Tags) + state := params.Events.GetCurrentState(params.Throttled) emoji := formatter.emojiGetter.GetStateEmoji(state) title := formatter.buildTitle(params.Events, params.Trigger, emoji, params.Throttled) - titleLen := calcRunesCountWithoutHTML([]rune(title)) + titleLen := calcRunesCountWithoutHTML(title) + len("\n") + + var tags string + var tagsLen int + + triggerTags := params.Trigger.GetTags() + if len(triggerTags) != 0 { + tags = " " + triggerTags + tagsLen = calcRunesCountWithoutHTML(tags) + } desc := descriptionFormatter(params.Trigger) - descLen := calcRunesCountWithoutHTML([]rune(desc)) + descLen := calcRunesCountWithoutHTML(desc) - eventsString := formatter.buildEventsString(params.Events, -1, params.Throttled) - eventsStringLen := calcRunesCountWithoutHTML([]rune(eventsString)) + events := formatter.buildEventsString(params.Events, -1, params.Throttled) + eventsStringLen := calcRunesCountWithoutHTML(events) - descNewLen, eventsNewLen := senders.CalculateMessagePartsLength(params.MessageMaxChars-titleLen, descLen, eventsStringLen) + tagsNewLen, descNewLen, eventsNewLen := senders.CalculateMessagePartsBetweenTagsDescEvents(params.MessageMaxChars-titleLen, tagsLen, descLen, eventsStringLen) + if tagsLen != tagsNewLen { + tags = msgformat.DefaultTagsLimiter(params.Trigger.Tags, tagsNewLen) + } if descLen != descNewLen { desc = descriptionCutter(desc, descNewLen) } if eventsStringLen != eventsNewLen { - eventsString = formatter.buildEventsString(params.Events, eventsNewLen, params.Throttled) + events = formatter.buildEventsString(params.Events, eventsNewLen, params.Throttled) + } + + return title + tags + "\n" + desc + events +} + +func htmlEscapeTags(tags []string) []string { + escapedTags := make([]string, 0, len(tags)) + + for _, tag := range tags { + escapedTags = append(escapedTags, html.EscapeString(tag)) } - return title + desc + eventsString + return escapedTags } // calcRunesCountWithoutHTML is used for calculating symbols in text without html tags. Special symbols // like `>`, `<` etc. are counted not as one symbol, for example, len([]rune(">")). // This precision is enough for us to evaluate size of message. -func calcRunesCountWithoutHTML(htmlText []rune) int { +func calcRunesCountWithoutHTML(htmlText string) int { textLen := 0 isTag := false @@ -109,12 +133,6 @@ func (formatter *messageFormatter) buildTitle(events moira.NotificationEvents, t title += " " + trigger.Name } - tags := trigger.GetTags() - if tags != "" { - title += " " + tags - } - - title += "\n" return title } @@ -125,7 +143,7 @@ var throttleMsg = fmt.Sprintf("\nPlease, %s to generate less events.", boldForma func (formatter *messageFormatter) buildEventsString(events moira.NotificationEvents, charsForEvents int, throttled bool) string { charsForThrottleMsg := 0 if throttled { - charsForThrottleMsg = calcRunesCountWithoutHTML([]rune(throttleMsg)) + charsForThrottleMsg = calcRunesCountWithoutHTML(throttleMsg) } charsLeftForEvents := charsForEvents - charsForThrottleMsg @@ -144,7 +162,7 @@ func (formatter *messageFormatter) buildEventsString(events moira.NotificationEv tailString = fmt.Sprintf("\n...and %d more events.", len(events)-eventsPrinted) tailStringLen := len("\n") + utf8.RuneCountInString(tailString) - lineLen := calcRunesCountWithoutHTML([]rune(line)) + lineLen := calcRunesCountWithoutHTML(line) if charsForEvents >= 0 && eventsStringLen+lineLen > charsLeftForEvents-tailStringLen { eventsLenLimitReached = true diff --git a/senders/telegram/message_formatter_test.go b/senders/telegram/message_formatter_test.go index f6ed08cb2..f28308cfc 100644 --- a/senders/telegram/message_formatter_test.go +++ b/senders/telegram/message_formatter_test.go @@ -1,7 +1,6 @@ package telegram import ( - "fmt" "strings" "testing" "time" @@ -43,8 +42,6 @@ func TestMessageFormatter_Format(t *testing.T) { } expectedFirstLine := "💣 NODATA Name [tag1][tag2]\n" - lenFirstLine := utf8.RuneCountInString(expectedFirstLine) - - utf8.RuneCountInString("") eventStr := "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n" lenEventStr := utf8.RuneCountInString(eventStr) - utf8.RuneCountInString("") // 60 - 13 = 47 @@ -109,86 +106,182 @@ func TestMessageFormatter_Format(t *testing.T) { }) Convey("with long messages", func() { - msgLimit := albumCaptionMaxCharacters - lenFirstLine - halfMsgLimit := msgLimit / 2 - greaterThanHalf := halfMsgLimit + 100 - lessThanHalf := halfMsgLimit - 100 + const ( + titleWithoutTags = "💣 NODATA Name" + ) + + titleLen := utf8.RuneCountInString(titleWithoutTags) - + utf8.RuneCountInString("") + len("\n") // 70 - 57 + 1 = 14 + + msgLimit := albumCaptionMaxCharacters - titleLen // 1024 - 14 = 1010 + thirdOfMsgLimit := msgLimit / 3 + greaterThanThird := thirdOfMsgLimit + 150 + lessThanThird := thirdOfMsgLimit - 100 + + // see genDescByLimit + symbolAtEndOfDescription := "" + if thirdOfMsgLimit%2 != 0 { + symbolAtEndOfDescription = "i" + } + + Convey("with tags > msgLimit/3, desc and events < msgLimit/3", func() { + trigger.Tags = genTagsByLimit(greaterThanThird + 200) + trigger.Desc = genDescByLimit(lessThanThird) + events := genEventsByLimit(event, lenEventStr, lessThanThird) + + expected := titleWithoutTags + + msgformat.DefaultTagsLimiter(trigger.Tags, + msgLimit-lessThanThird-len(events)*lenEventStr-len("\n")) + "\n" + + strings.Repeat("ёж", lessThanThird/2) + symbolAtEndOfDescription + "\n" + + eventsBlockStart + "\n" + + strings.Repeat(eventStr, len(events)) + eventsBlockEnd - Convey("text size of description > msgLimit / 2", func() { - var events moira.NotificationEvents - throttled := false + actual := formatter.Format(getParams(events, trigger, false)) - eventsCount := lessThanHalf / lenEventStr - for i := 0; i < eventsCount; i++ { - events = append(events, event) - } + So(actual, ShouldResemble, expected) + So(calcRunesCountWithoutHTML(actual), ShouldBeLessThanOrEqualTo, albumCaptionMaxCharacters) + }) - trigger.Desc = strings.Repeat("**ё**ж", greaterThanHalf/2) + Convey("with desc > msgLimit/3, tags and events < msgLimit/3", func() { + trigger.Tags = genTagsByLimit(lessThanThird) + trigger.Desc = genDescByLimit(greaterThanThird + 200) + events := genEventsByLimit(event, lenEventStr, lessThanThird) - expected := expectedFirstLine + - strings.Repeat("ёж", greaterThanHalf/2) + "\n" + + expected := titleWithoutTags + + msgformat.DefaultTagsLimiter(trigger.Tags, lessThanThird) + "\n" + + tooLongDescMessage + eventsBlockStart + "\n" + - strings.Repeat(eventStr, eventsCount) + - eventsBlockEnd + strings.Repeat(eventStr, len(events)) + eventsBlockEnd - msg := formatter.Format(getParams(events, trigger, throttled)) + actual := formatter.Format(getParams(events, trigger, false)) - So(calcRunesCountWithoutHTML([]rune(msg)), ShouldBeLessThanOrEqualTo, albumCaptionMaxCharacters) - So(msg, ShouldEqual, expected) + So(actual, ShouldResemble, expected) + So(calcRunesCountWithoutHTML(actual), ShouldBeLessThanOrEqualTo, albumCaptionMaxCharacters) }) - Convey("text size of events block > msgLimit / 2", func() { - var events moira.NotificationEvents - throttled := false + Convey("with events > msgLimit/3, tags and desc < msgLimit/3", func() { + trigger.Tags = genTagsByLimit(lessThanThird) + trigger.Desc = genDescByLimit(lessThanThird) + events := genEventsByLimit(event, lenEventStr, greaterThanThird+200) + + expected := titleWithoutTags + + msgformat.DefaultTagsLimiter(trigger.Tags, lessThanThird) + "\n" + + strings.Repeat("ёж", lessThanThird/2) + symbolAtEndOfDescription + "\n" + + eventsBlockStart + "\n" + + strings.Repeat(eventStr, 10) + eventsBlockEnd + + "\n...and 4 more events." + + actual := formatter.Format(getParams(events, trigger, false)) - eventsCount := greaterThanHalf / lenEventStr - for i := 0; i < eventsCount; i++ { - events = append(events, event) - } + So(actual, ShouldResemble, expected) + So(calcRunesCountWithoutHTML(actual), ShouldBeLessThanOrEqualTo, albumCaptionMaxCharacters) + }) - trigger.Desc = strings.Repeat("**ё**ж", lessThanHalf/2) + Convey("with tags and desc > msgLimit/3, events < msgLimit/3", func() { + trigger.Tags = genTagsByLimit(greaterThanThird) + trigger.Desc = genDescByLimit(greaterThanThird) + events := genEventsByLimit(event, lenEventStr, lessThanThird) - expected := expectedFirstLine + - strings.Repeat("ёж", lessThanHalf/2) + "\n" + + expected := titleWithoutTags + + msgformat.DefaultTagsLimiter(trigger.Tags, thirdOfMsgLimit) + "\n" + + tooLongDescMessage + eventsBlockStart + "\n" + - strings.Repeat(eventStr, eventsCount) + - eventsBlockEnd + strings.Repeat(eventStr, len(events)) + eventsBlockEnd - msg := formatter.Format(getParams(events, trigger, throttled)) + actual := formatter.Format(getParams(events, trigger, false)) - So(calcRunesCountWithoutHTML([]rune(msg)), ShouldBeLessThanOrEqualTo, albumCaptionMaxCharacters) - So(msg, ShouldEqual, expected) + So(actual, ShouldResemble, expected) + So(calcRunesCountWithoutHTML(actual), ShouldBeLessThanOrEqualTo, albumCaptionMaxCharacters) }) - Convey("both description and events block have text size > msgLimit/2", func() { - var events moira.NotificationEvents - throttled := false + Convey("with tags and events > msgLimit / 3, desc < msgLimit/3", func() { + trigger.Tags = genTagsByLimit(greaterThanThird) + trigger.Desc = genDescByLimit(lessThanThird) + events := genEventsByLimit(event, lenEventStr, greaterThanThird) - eventsCount := greaterThanHalf / lenEventStr - for i := 0; i < eventsCount; i++ { - events = append(events, event) - } + expected := titleWithoutTags + + msgformat.DefaultTagsLimiter(trigger.Tags, thirdOfMsgLimit) + "\n" + + strings.Repeat("ёж", lessThanThird/2) + symbolAtEndOfDescription + "\n" + + eventsBlockStart + "\n" + + strings.Repeat(eventStr, 8) + eventsBlockEnd + + "\n...and 2 more events." + + actual := formatter.Format(getParams(events, trigger, false)) - trigger.Desc = strings.Repeat("**ё**ж", greaterThanHalf/2) + So(actual, ShouldResemble, expected) + So(calcRunesCountWithoutHTML(actual), ShouldBeLessThanOrEqualTo, albumCaptionMaxCharacters) + }) - eventsShouldBe := halfMsgLimit / lenEventStr + Convey("with desc and events > msgLimit / 3, tags < msgLimit/3", func() { + trigger.Tags = genTagsByLimit(lessThanThird) + trigger.Desc = genDescByLimit(greaterThanThird) + events := genEventsByLimit(event, lenEventStr, greaterThanThird) - expected := expectedFirstLine + + expected := titleWithoutTags + + msgformat.DefaultTagsLimiter(trigger.Tags, lessThanThird) + "\n" + tooLongDescMessage + eventsBlockStart + "\n" + - strings.Repeat(eventStr, eventsShouldBe) + + strings.Repeat(eventStr, 7) + eventsBlockEnd + + "\n...and 3 more events." + + actual := formatter.Format(getParams(events, trigger, false)) + + So(actual, ShouldResemble, expected) + So(calcRunesCountWithoutHTML(actual), ShouldBeLessThanOrEqualTo, albumCaptionMaxCharacters) + }) + + Convey("with tags, desc, events > msgLimit/3", func() { + trigger.Tags = genTagsByLimit(greaterThanThird) + trigger.Desc = genDescByLimit(greaterThanThird) + events := genEventsByLimit(event, lenEventStr, greaterThanThird) + + expected := titleWithoutTags + + msgformat.DefaultTagsLimiter(trigger.Tags, thirdOfMsgLimit) + "\n" + + tooLongDescMessage + + eventsBlockStart + "\n" + + strings.Repeat(eventStr, 6) + eventsBlockEnd + - fmt.Sprintf("\n...and %d more events.", len(events)-eventsShouldBe) + "\n...and 4 more events." - msg := formatter.Format(getParams(events, trigger, throttled)) + actual := formatter.Format(getParams(events, trigger, false)) - So(calcRunesCountWithoutHTML([]rune(msg)), ShouldBeLessThanOrEqualTo, albumCaptionMaxCharacters) - So(msg, ShouldEqual, expected) + So(actual, ShouldResemble, expected) + So(calcRunesCountWithoutHTML(actual), ShouldBeLessThanOrEqualTo, albumCaptionMaxCharacters) }) }) }) } +func genTagsByLimit(limit int) []string { + tagName := "tag1" + + tagsCount := (limit - 1) / (len(tagName) + 2) + + tags := make([]string, 0, tagsCount) + + for i := 0; i < tagsCount; i++ { + tags = append(tags, tagName) + } + + return tags +} + +func genDescByLimit(limit int) string { + str := strings.Repeat("**ё**ж", limit/2) + if limit%2 != 0 { + str += "i" + } + return str +} + +func genEventsByLimit(event moira.NotificationEvent, oneEventLineLen int, limit int) moira.NotificationEvents { + var events moira.NotificationEvents + for i := 0; i < limit/oneEventLineLen; i++ { + events = append(events, event) + } + return events +} + func getParams(events moira.NotificationEvents, trigger moira.TriggerData, throttled bool) msgformat.MessageFormatterParams { return msgformat.MessageFormatterParams{ Events: events,