1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
|
package util
import (
"fmt"
"strconv"
"strings"
"github.com/onsi/ginkgo/v2"
"github.com/onsi/gomega"
"github.com/nicholas-fedor/shoutrrr/pkg/types"
)
var _ = ginkgo.Describe("Partition Message", func() {
limits := types.MessageLimit{
ChunkSize: 2000,
TotalChunkSize: 6000,
ChunkCount: 10,
}
ginkgo.When("given a message that exceeds the max length", func() {
ginkgo.When("not splitting by lines", func() {
ginkgo.It("should return a payload with chunked messages", func() {
items, _ := testPartitionMessage(42)
gomega.Expect(items[0].Text).To(gomega.HaveLen(1994))
gomega.Expect(items[1].Text).To(gomega.HaveLen(1999))
gomega.Expect(items[2].Text).To(gomega.HaveLen(205))
})
ginkgo.It("omit characters above total max", func() {
items, _ := testPartitionMessage(62)
gomega.Expect(items[0].Text).To(gomega.HaveLen(1994))
gomega.Expect(items[1].Text).To(gomega.HaveLen(1999))
gomega.Expect(items[2].Text).To(gomega.HaveLen(1999))
gomega.Expect(items[3].Text).To(gomega.HaveLen(5))
})
ginkgo.It("should handle messages with a size modulus of chunksize", func() {
items, _ := testPartitionMessage(20)
// Last word fits in the chunk size
gomega.Expect(items[0].Text).To(gomega.HaveLen(2000))
items, _ = testPartitionMessage(40)
// Now the last word of the first chunk will be concatenated with
// the first word of the second chunk, and so it does not fit in the chunk anymore
gomega.Expect(items[0].Text).To(gomega.HaveLen(1994))
gomega.Expect(items[1].Text).To(gomega.HaveLen(1999))
gomega.Expect(items[2].Text).To(gomega.HaveLen(5))
})
ginkgo.When("the message is empty", func() {
ginkgo.It("should return no items", func() {
items, _ := testPartitionMessage(0)
gomega.Expect(items).To(gomega.BeEmpty())
})
})
ginkgo.When("given an input without whitespace", func() {
ginkgo.It("should not crash, regardless of length", func() {
unalignedLimits := types.MessageLimit{
ChunkSize: 1997,
ChunkCount: 11,
TotalChunkSize: 5631,
}
testString := ""
for inputLen := 1; inputLen < 8000; inputLen++ {
// add a rune to the string using a repeatable pattern (single digit hex of position)
testString += strconv.FormatInt(int64(inputLen%16), 16)
items, omitted := PartitionMessage(testString, unalignedLimits, 7)
included := 0
for ii, item := range items {
expectedSize := unalignedLimits.ChunkSize
// The last chunk might be smaller than the preceding chunks
if ii == len(items)-1 {
// the chunk size is the remainder of, the total size,
// or the max size, whatever is smallest,
// and the previous chunk sizes
chunkSize := Min(
inputLen,
unalignedLimits.TotalChunkSize,
) % unalignedLimits.ChunkSize
// if the "rest" of the runes needs another chunk
if chunkSize > 0 {
// expect the chunk to contain the "rest" of the runes
expectedSize = chunkSize
}
// the last chunk should never be empty, so treat it as one of the full ones
}
// verify the data, but only on the last chunk to reduce test time
if ii == len(items)-1 {
for ri, r := range item.Text {
runeOffset := (len(item.Text) - ri) - 1
runeVal, err := strconv.ParseInt(string(r), 16, 64)
expectedLen := Min(inputLen, unalignedLimits.TotalChunkSize)
expectedVal := (expectedLen - runeOffset) % 16
gomega.Expect(err).ToNot(gomega.HaveOccurred())
gomega.Expect(runeVal).To(gomega.Equal(int64(expectedVal)))
}
}
included += len(item.Text)
gomega.Expect(item.Text).To(gomega.HaveLen(expectedSize))
}
gomega.Expect(omitted + included).To(gomega.Equal(inputLen))
}
})
})
})
ginkgo.When("splitting by lines", func() {
ginkgo.It("should return a payload with chunked messages", func() {
batches := testMessageItemsFromLines(18, limits, 2)
items := batches[0]
gomega.Expect(items[0].Text).To(gomega.HaveLen(200))
gomega.Expect(items[8].Text).To(gomega.HaveLen(200))
})
ginkgo.When("the message items exceed the limits", func() {
ginkgo.It("should split items into multiple batches", func() {
batches := testMessageItemsFromLines(21, limits, 2)
for b, chunks := range batches {
fmt.Fprintf(ginkgo.GinkgoWriter, "Batch #%v: (%v chunks)\n", b, len(chunks))
for c, chunk := range chunks {
fmt.Fprintf(
ginkgo.GinkgoWriter,
" - Chunk #%v: (%v runes)\n",
c,
len(chunk.Text),
)
}
}
gomega.Expect(batches).To(gomega.HaveLen(2))
})
})
ginkgo.It("should trim characters above chunk size", func() {
hundreds := 42
repeat := 21
batches := testMessageItemsFromLines(hundreds, limits, repeat)
items := batches[0]
gomega.Expect(items[0].Text).To(gomega.HaveLen(limits.ChunkSize))
gomega.Expect(items[1].Text).To(gomega.HaveLen(limits.ChunkSize))
})
})
})
})
const hundredChars = "this string is exactly (to the letter) a hundred characters long which will make the send func error"
// testMessageItemsFromLines generates message item batches from repeated text with line breaks.
func testMessageItemsFromLines(
hundreds int,
limits types.MessageLimit,
repeat int,
) [][]types.MessageItem {
builder := strings.Builder{}
ri := 0
for range hundreds {
builder.WriteString(hundredChars)
ri++
if ri == repeat {
builder.WriteRune('\n')
ri = 0
}
}
return MessageItemsFromLines(builder.String(), limits)
}
// testPartitionMessage partitions repeated text into message items.
func testPartitionMessage(hundreds int) ([]types.MessageItem, int) {
limits := types.MessageLimit{
ChunkSize: 2000,
TotalChunkSize: 6000,
ChunkCount: 10,
}
builder := strings.Builder{}
for range hundreds {
builder.WriteString(hundredChars)
}
items, omitted := PartitionMessage(builder.String(), limits, 100)
contentSize := Min(hundreds*100, limits.TotalChunkSize)
expectedOmitted := Max(0, (hundreds*100)-contentSize)
gomega.ExpectWithOffset(0, omitted).To(gomega.Equal(expectedOmitted))
return items, omitted
}
|