package queue import ( "strings" "testing" "time" ) // TestTruncateText: Kürzung auf max Runen an einer Wortgrenze (nicht in der // ersten Hälfte), ohne hängende Satzzeichen; kurze Texte und max 0 bleiben // unangetastet; Runen zählen, nicht Bytes. func TestTruncateText(t *testing.T) { cases := []struct { name string in string max int want string truncated bool }{ {"kurz bleibt", "Build fertig", 400, "Build fertig", false}, {"genau max bleibt", "abcde", 5, "abcde", false}, {"unbegrenzt", strings.Repeat("x", 1000), 0, strings.Repeat("x", 1000), false}, {"wortgrenze", "Der Build ist fertig und bereit", 14, "Der Build ist", true}, {"satzzeichen weg", "Fertig, aber dann kam noch mehr", 8, "Fertig", true}, {"keine wortgrenze in zweiter hälfte → hart", "abcdefghijklmnop qrs", 10, "abcdefghij", true}, {"umlaute als eine 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("Ergebnis %q länger als %d Runen", got, c.max) } }) } } // TestEnqueueKuerztText: Das Gesetz MaxText greift beim Eintritt — die // gekürzte Fassung wird gesprochen, Admission meldet Truncated. func TestEnqueueKuerztText(t *testing.T) { g := neueSpielerin() q := New(Laws{MaxText: 20}, g.speak, nil) defer q.Close() adm, err := q.Enqueue(Item{Text: "Dieser Text ist deutlich länger als zwanzig Zeichen"}) if err != nil || adm.ID == "" || !adm.Truncated { t.Fatalf("Enqueue = %+v, %v; want aufgenommen + Truncated", adm, err) } items := g.warteBis(t, 1) if items[0].Text != "Dieser Text ist" { t.Errorf("gesprochen = %q, want gekürzte Fassung", items[0].Text) } g.step <- struct{}{} adm, _ = q.Enqueue(Item{Text: "kurz"}) if adm.Truncated { t.Error("kurzer Text als gekürzt gemeldet") } g.warteBis(t, 2) g.step <- struct{}{} } // TestDecideExpired: Ein Item, das länger als MaxAge gewartet hat, wird // verworfen — vor Ruhezeit und Rate-Limit, aber nach now (now bricht aus). func TestDecideExpired(t *testing.T) { laws := Laws{MaxAge: time.Minute, RateLimit: time.Hour} now := time.Now() alt := Item{Text: "alt", Priority: PriorityNormal, enqueued: now.Add(-2 * time.Minute)} frisch := Item{Text: "frisch", Priority: PriorityNormal, enqueued: now.Add(-10 * time.Second)} if v := laws.decide(alt, now, true); v != verdictDiscardExpired { t.Errorf("altes Item bei freiem Slot = %v, want verdictDiscardExpired", v) } if v := laws.decide(frisch, now, true); v != verdictPlay { t.Errorf("frisches Item bei freiem Slot = %v, want verdictPlay", v) } if v := laws.decide(frisch, now, false); v != verdictWait { t.Errorf("frisches Item ohne Slot = %v, want verdictWait", v) } altNow := alt altNow.Now = true if v := laws.decide(altNow, now, false); v != verdictPlay { t.Errorf("altes now-Item = %v, want verdictPlay (now bricht aus)", v) } if v := (Laws{}).decide(alt, now, true); v != verdictPlay { t.Errorf("MaxAge 0 (unbegrenzt) verwirft altes Item: %v", v) } } // TestGeparktesItemVerfaellt: Ein normal-Item parkt auf den Rate-Limit-Slot; // ist es beim Fälligwerden älter als MaxAge, wird es verworfen (Protokoll // nennt max_age) und nicht mehr gesprochen. func TestGeparktesItemVerfaellt(t *testing.T) { logb := &syncLog{} g := neueSpielerin() q := New(Laws{RateLimit: 300 * time.Millisecond, MaxAge: 150 * time.Millisecond}, g.speak, logb) defer q.Close() q.Enqueue(Item{Text: "erste"}) // bucht das Fenster g.warteBis(t, 1) g.step <- struct{}{} adm, _ := q.Enqueue(Item{Text: "parkt zu lange"}) // parkt ~300 ms > MaxAge if adm.ID == "" || adm.Discarded != "" { t.Fatalf("beim Eintritt darf nichts verfallen: %+v", adm) } waitFor(t, func() bool { return strings.Contains(logb.String(), "max_age") }) g.keineWeitere(t) if !strings.Contains(logb.String(), adm.ID) { t.Errorf("Protokoll nennt die verfallene Ansage nicht:\n%s", logb.String()) } }