1
0
Fork 0
golang-github-nicholas-fedo.../pkg/util/partition_message_test.go
Daniel Baumann c0c4addb85
Adding upstream version 0.8.9.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-05-22 10:16:14 +02:00

193 lines
6.2 KiB
Go

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
}