package queue import ( "strings" "testing" "time" ) // 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. func TestTruncateText(t *testing.T) { cases := []struct { name string in string max int want string truncated bool }{ {"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}, } 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 { t.Errorf("result %q longer than %d runes", got, c.max) } }) } } // TestEnqueueTruncatesText: the MaxText law applies on admission; the // shortened version is spoken and Admission reports Truncated. func TestEnqueueTruncatesText(t *testing.T) { g := newSpeaker() q := New(Laws{MaxText: 20}, g.speak, nil) defer q.Close() adm, err := q.Enqueue(Item{Text: "This text is clearly longer than twenty characters"}) if err != nil || adm.ID == "" || !adm.Truncated { t.Fatalf("Enqueue = %+v, %v; want admitted + Truncated", adm, err) } items := g.waitUntil(t, 1) if items[0].Text != "This text is clearly" { t.Errorf("spoken = %q, want the shortened version", items[0].Text) } g.step <- struct{}{} adm, _ = q.Enqueue(Item{Text: "short"}) if adm.Truncated { t.Error("short text reported as truncated") } g.waitUntil(t, 2) g.step <- struct{}{} } // TestDecideExpired: an item that waited longer than MaxAge is discarded, // before quiet hours and rate limit but after now (now breaks out). func TestDecideExpired(t *testing.T) { laws := Laws{MaxAge: time.Minute, RateLimit: time.Hour} now := time.Now() old := Item{Text: "old", Priority: PriorityNormal, enqueued: now.Add(-2 * time.Minute)} fresh := Item{Text: "fresh", Priority: PriorityNormal, enqueued: now.Add(-10 * time.Second)} if v := laws.decide(old, now, true); v != verdictDiscardExpired { t.Errorf("old item with open slot = %v, want verdictDiscardExpired", v) } if v := laws.decide(fresh, now, true); v != verdictPlay { t.Errorf("fresh item with open slot = %v, want verdictPlay", v) } if v := laws.decide(fresh, now, false); v != verdictWait { t.Errorf("fresh item without slot = %v, want verdictWait", v) } 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) } if v := (Laws{}).decide(old, now, true); v != verdictPlay { t.Errorf("MaxAge 0 (unlimited) discards old item: %v", v) } } // 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) { logb := &syncLog{} g := newSpeaker() q := New(Laws{RateLimit: 300 * time.Millisecond, MaxAge: 150 * time.Millisecond}, g.speak, logb) defer q.Close() q.Enqueue(Item{Text: "first"}) // books the window g.waitUntil(t, 1) g.step <- struct{}{} adm, _ := q.Enqueue(Item{Text: "parks too long"}) // parks ~300 ms > MaxAge if adm.ID == "" || adm.Discarded != "" { t.Fatalf("nothing may expire on admission: %+v", adm) } waitFor(t, func() bool { return strings.Contains(logb.String(), "max_age") }) g.noFurther(t) if !strings.Contains(logb.String(), adm.ID) { t.Errorf("log does not name the expired announcement:\n%s", logb.String()) } }