Better notification organization.
This commit is contained in:
1 parent
81cc630319
commit
2c02a79655
14 files changed
+1276
-40
No files matched your search
@@ -13,6 +13,7 @@ import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -23,6 +24,125 @@ import (
|
||||
"git.stormux.org/storm/fenrir/internal/presentation"
|
||||
)
|
||||
|
||||
func TestNotificationSoundCooldownBoundaryAndConcurrentAdmission(t *testing.T) {
|
||||
var gate notificationSoundGate
|
||||
now := time.Now()
|
||||
var workers sync.WaitGroup
|
||||
accepted := make(chan bool, 32)
|
||||
for range cap(accepted) {
|
||||
workers.Go(func() { accepted <- gate.allow(now) })
|
||||
}
|
||||
workers.Wait()
|
||||
close(accepted)
|
||||
count := 0
|
||||
for allowed := range accepted {
|
||||
if allowed {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count != 1 {
|
||||
t.Fatal("concurrent notifications admitted multiple cues", count)
|
||||
}
|
||||
for _, trial := range []struct {
|
||||
after time.Duration
|
||||
want bool
|
||||
}{
|
||||
{time.Second, false},
|
||||
{30*time.Second - time.Nanosecond, false},
|
||||
{30 * time.Second, true},
|
||||
{31 * time.Second, false},
|
||||
{60 * time.Second, true},
|
||||
} {
|
||||
if got := gate.allow(now.Add(trial.after)); got != trial.want {
|
||||
t.Fatalf("after %v: allowed=%v, want %v", trial.after, got, trial.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotificationBurstLimitsCuesWithoutLosingSpeechOrHistory(t *testing.T) {
|
||||
for _, mode := range []string{"enabled", "muted", "silenced"} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
a, voice := persistence_application(t, true)
|
||||
voice.messages = nil
|
||||
sounds := &fakeSounds{}
|
||||
a.cues = sounds
|
||||
a.muted, voice.muted = mode == "muted", mode == "muted"
|
||||
a.temporarySilence = mode == "silenced"
|
||||
var want []string
|
||||
for index := range 10 {
|
||||
text := fmt.Sprintf("Notification %d", index)
|
||||
want = append(want, text)
|
||||
a.notify(text, time.Now())
|
||||
if len(sounds.cues) != 1 || len(a.notifications) != index+1 || a.notifications[index].text != text {
|
||||
t.Fatal("burst cue or retained message", sounds.cues, a.notifications)
|
||||
}
|
||||
}
|
||||
if mode != "enabled" {
|
||||
want = nil
|
||||
}
|
||||
if !reflect.DeepEqual(voice.messages, want) {
|
||||
t.Fatal("burst changed speech policy", voice.messages)
|
||||
}
|
||||
// A mute transition shares the cooldown with ordinary notifications.
|
||||
a.muted, voice.muted, a.temporarySilence = false, false, false
|
||||
a.notify("Still cooling down", time.Now())
|
||||
if len(sounds.cues) != 1 || voice.messages[len(voice.messages)-1] != "Still cooling down" {
|
||||
t.Fatal("unmuting reset the cooldown", sounds.cues, voice.messages)
|
||||
}
|
||||
a.notificationSound.mu.Lock()
|
||||
a.notificationSound.lastPlay = time.Now().Add(-notificationSoundInterval)
|
||||
a.notificationSound.mu.Unlock()
|
||||
a.notify("After cooldown", time.Now())
|
||||
if len(sounds.cues) != 2 || voice.messages[len(voice.messages)-1] != "After cooldown" || len(a.notifications) != 12 {
|
||||
t.Fatal("cue did not resume after cooldown", sounds.cues, voice.messages, a.notifications)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type queuedNotificationVoice struct {
|
||||
fakeVoice
|
||||
requests []struct {
|
||||
text string
|
||||
before func(context.Context)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *queuedNotificationVoice) Say_after(text string, before func(context.Context)) {
|
||||
v.requests = append(v.requests, struct {
|
||||
text string
|
||||
before func(context.Context)
|
||||
}{text, before})
|
||||
}
|
||||
|
||||
func TestQueuedNotificationsCheckCooldownAtPlaybackAndCancellation(t *testing.T) {
|
||||
a, _ := persistence_application(t, true)
|
||||
voice := &queuedNotificationVoice{}
|
||||
a.voice.Speaker = voice
|
||||
sounds := &fakeSounds{}
|
||||
a.cues = sounds
|
||||
a.notify("Canceled", time.Now())
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
voice.requests[0].before(ctx)
|
||||
if !a.notificationSound.lastPlay.IsZero() || len(sounds.cues) != 0 {
|
||||
t.Fatal("canceled cue started the cooldown")
|
||||
}
|
||||
a.notify("First queued", time.Now())
|
||||
// More than 30 seconds can pass while notifications wait in the speech queue.
|
||||
a.notify("Second queued", time.Now().Add(time.Minute))
|
||||
if !a.notificationSound.lastPlay.IsZero() || len(sounds.cues) != 0 {
|
||||
t.Fatal("cue cooldown started before playback")
|
||||
}
|
||||
for _, request := range voice.requests[1:] {
|
||||
request.before(context.Background())
|
||||
voice.Say(request.text)
|
||||
}
|
||||
if !reflect.DeepEqual(sounds.cues, []presentation.Cue{presentation.Notification}) || !reflect.DeepEqual(voice.messages, []string{"First queued", "Second queued"}) {
|
||||
t.Fatal("queued notifications accumulated sound waits or lost speech", sounds.cues, voice.messages)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotificationCuePrecedesSpeechAndOnlyPlaysForNewNotifications(t *testing.T) {
|
||||
for _, mode := range []string{"enabled", "disabled", "zero volume", "missing cue", "unavailable", "muted", "silenced"} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
|
||||
Reference in new issue
Block a user