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,