2026-10-24 09:54:40 +02:00
|
|
|
package queue
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// TestTruncateText: cut to max runes at a word boundary (not in the first
|
|
|
|
|
// half), without dangling punctuation; short texts and max 0 stay untouched;
|
|
|
|
|
// runes count, not bytes.
|
2026-10-24 09:54:40 +02:00
|
|
|
func TestTruncateText(t *testing.T) {
|
|
|
|
|
cases := []struct {
|
|
|
|
|
name string
|
|
|
|
|
in string
|
|
|
|
|
max int
|
|
|
|
|
want string
|
|
|
|
|
truncated bool
|
|
|
|
|
}{
|
2026-10-24 11:15:35 +02:00
|
|
|
{"short stays", "Build done", 400, "Build done", false},
|
|
|
|
|
{"exactly max stays", "abcde", 5, "abcde", false},
|
|
|
|
|
{"unlimited", strings.Repeat("x", 1000), 0, strings.Repeat("x", 1000), false},
|
|
|
|
|
{"word boundary", "The build is done and ready", 14, "The build is", true},
|
|
|
|
|
{"punctuation dropped", "Done, but then there was more", 6, "Done", true},
|
|
|
|
|
{"no boundary in second half → hard cut", "abcdefghijklmnop qrs", 10, "abcdefghij", true},
|
|
|
|
|
{"umlauts count as one rune", "Übergröße ärgert", 9, "Übergröße", true},
|
2026-10-24 09:54:40 +02:00
|
|
|
}
|
|
|
|
|
for _, c := range cases {
|
|
|
|
|
t.Run(c.name, func(t *testing.T) {
|
|
|
|
|
got, tr := truncateText(c.in, c.max)
|
|
|
|
|
if got != c.want || tr != c.truncated {
|
|
|
|
|
t.Errorf("truncateText(%q, %d) = %q/%v, want %q/%v", c.in, c.max, got, tr, c.want, c.truncated)
|
|
|
|
|
}
|
|
|
|
|
if c.max > 0 && len([]rune(got)) > c.max {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Errorf("result %q longer than %d runes", got, c.max)
|
2026-10-24 09:54:40 +02:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// TestEnqueueTruncatesText: the MaxText law applies on admission; the
|
|
|
|
|
// shortened version is spoken and Admission reports Truncated.
|
|
|
|
|
func TestEnqueueTruncatesText(t *testing.T) {
|
|
|
|
|
g := newSpeaker()
|
2026-10-24 09:54:40 +02:00
|
|
|
q := New(Laws{MaxText: 20}, g.speak, nil)
|
|
|
|
|
defer q.Close()
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
adm, err := q.Enqueue(Item{Text: "This text is clearly longer than twenty characters"})
|
2026-10-24 09:54:40 +02:00
|
|
|
if err != nil || adm.ID == "" || !adm.Truncated {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Fatalf("Enqueue = %+v, %v; want admitted + Truncated", adm, err)
|
2026-10-24 09:54:40 +02:00
|
|
|
}
|
2026-10-24 11:15:35 +02:00
|
|
|
items := g.waitUntil(t, 1)
|
|
|
|
|
if items[0].Text != "This text is clearly" {
|
|
|
|
|
t.Errorf("spoken = %q, want the shortened version", items[0].Text)
|
2026-10-24 09:54:40 +02:00
|
|
|
}
|
|
|
|
|
g.step <- struct{}{}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
adm, _ = q.Enqueue(Item{Text: "short"})
|
2026-10-24 09:54:40 +02:00
|
|
|
if adm.Truncated {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Error("short text reported as truncated")
|
2026-10-24 09:54:40 +02:00
|
|
|
}
|
2026-10-24 11:15:35 +02:00
|
|
|
g.waitUntil(t, 2)
|
2026-10-24 09:54:40 +02:00
|
|
|
g.step <- struct{}{}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// TestDecideExpired: an item that waited longer than MaxAge is discarded,
|
|
|
|
|
// before quiet hours and rate limit but after now (now breaks out).
|
2026-10-24 09:54:40 +02:00
|
|
|
func TestDecideExpired(t *testing.T) {
|
|
|
|
|
laws := Laws{MaxAge: time.Minute, RateLimit: time.Hour}
|
|
|
|
|
now := time.Now()
|
2026-10-24 11:15:35 +02:00
|
|
|
old := Item{Text: "old", Priority: PriorityNormal, enqueued: now.Add(-2 * time.Minute)}
|
|
|
|
|
fresh := Item{Text: "fresh", Priority: PriorityNormal, enqueued: now.Add(-10 * time.Second)}
|
2026-10-24 09:54:40 +02:00
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
if v := laws.decide(old, now, true); v != verdictDiscardExpired {
|
|
|
|
|
t.Errorf("old item with open slot = %v, want verdictDiscardExpired", v)
|
2026-10-24 09:54:40 +02:00
|
|
|
}
|
2026-10-24 11:15:35 +02:00
|
|
|
if v := laws.decide(fresh, now, true); v != verdictPlay {
|
|
|
|
|
t.Errorf("fresh item with open slot = %v, want verdictPlay", v)
|
2026-10-24 09:54:40 +02:00
|
|
|
}
|
2026-10-24 11:15:35 +02:00
|
|
|
if v := laws.decide(fresh, now, false); v != verdictWait {
|
|
|
|
|
t.Errorf("fresh item without slot = %v, want verdictWait", v)
|
2026-10-24 09:54:40 +02:00
|
|
|
}
|
2026-10-24 11:15:35 +02:00
|
|
|
oldNow := old
|
|
|
|
|
oldNow.Now = true
|
|
|
|
|
if v := laws.decide(oldNow, now, false); v != verdictPlay {
|
|
|
|
|
t.Errorf("old now item = %v, want verdictPlay (now breaks out)", v)
|
2026-10-24 09:54:40 +02:00
|
|
|
}
|
2026-10-24 11:15:35 +02:00
|
|
|
if v := (Laws{}).decide(old, now, true); v != verdictPlay {
|
|
|
|
|
t.Errorf("MaxAge 0 (unlimited) discards old item: %v", v)
|
2026-10-24 09:54:40 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// TestParkedItemExpires: a normal item parks for the rate-limit slot; if it
|
|
|
|
|
// is older than MaxAge when it comes due, it is discarded (log names
|
|
|
|
|
// max_age) and never spoken.
|
|
|
|
|
func TestParkedItemExpires(t *testing.T) {
|
2026-10-24 09:54:40 +02:00
|
|
|
logb := &syncLog{}
|
2026-10-24 11:15:35 +02:00
|
|
|
g := newSpeaker()
|
2026-10-24 09:54:40 +02:00
|
|
|
q := New(Laws{RateLimit: 300 * time.Millisecond, MaxAge: 150 * time.Millisecond}, g.speak, logb)
|
|
|
|
|
defer q.Close()
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
q.Enqueue(Item{Text: "first"}) // books the window
|
|
|
|
|
g.waitUntil(t, 1)
|
2026-10-24 09:54:40 +02:00
|
|
|
g.step <- struct{}{}
|
2026-10-24 11:15:35 +02:00
|
|
|
adm, _ := q.Enqueue(Item{Text: "parks too long"}) // parks ~300 ms > MaxAge
|
2026-10-24 09:54:40 +02:00
|
|
|
if adm.ID == "" || adm.Discarded != "" {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Fatalf("nothing may expire on admission: %+v", adm)
|
2026-10-24 09:54:40 +02:00
|
|
|
}
|
|
|
|
|
waitFor(t, func() bool { return strings.Contains(logb.String(), "max_age") })
|
2026-10-24 11:15:35 +02:00
|
|
|
g.noFurther(t)
|
2026-10-24 09:54:40 +02:00
|
|
|
if !strings.Contains(logb.String(), adm.ID) {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Errorf("log does not name the expired announcement:\n%s", logb.String())
|
2026-10-24 09:54:40 +02:00
|
|
|
}
|
|
|
|
|
}
|