Files
cthulhu/internal/app/startup_sound_test.go

105 lines
3.1 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"context"
"log/slog"
"reflect"
"sync"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/config"
"git.stormux.org/storm/cthulhu/internal/sound"
)
// Hold the startup playback at the backend while application settings commit.
// No audio device or timing sleep is needed to observe synchronous cancellation.
type startupSoundPlayer struct {
started chan context.Context
release chan struct{}
once sync.Once
}
func (player *startupSoundPlayer) Play(ctx context.Context, _ []byte, _, _ float64) error {
first := false
player.once.Do(func() { first = true })
if first {
player.started <- ctx
select {
case <-ctx.Done():
return ctx.Err()
case <-player.release:
}
}
return nil
}
func TestApplicationSettingsPreserveStartupSoundUnlessSoundPolicyChanges(t *testing.T) {
for _, test := range []struct {
name string
change func(*config.Settings)
cancel bool
}{
{name: "same settings", change: func(*config.Settings) {}},
{name: "title reading override", change: func(settings *config.Settings) {
settings.Reading.AutomaticallyReadTitleChanges = false
}},
{name: "volume override", cancel: true, change: func(settings *config.Settings) {
settings.SoundVolume = 0.8
}},
{name: "disabled sound", cancel: true, change: func(settings *config.Settings) {
settings.EnableSound = false
}},
{name: "theme override", cancel: true, change: func(settings *config.Settings) {
settings.SoundTheme = "none"
}},
{name: "sink override", cancel: true, change: func(settings *config.Settings) {
settings.SoundSink = "pulse"
}},
} {
t.Run(test.name, func(t *testing.T) {
settings := config.DefaultSettings()
logger := slog.New(slog.DiscardHandler)
player := &startupSoundPlayer{started: make(chan context.Context, 1), release: make(chan struct{})}
service := sound.New(logger, player, settings.SoundTheme, settings.SoundVolume)
service.Configure(soundConfiguration(settings))
if err := service.Start(context.Background()); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
close(player.release)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if err := service.Stop(ctx); err != nil {
t.Error(err)
}
})
if !service.Enqueue(sound.Cue{Name: "start"}) {
t.Fatal("startup cue rejected")
}
var playback context.Context
select {
case playback = <-player.started:
case <-time.After(time.Second):
t.Fatal("startup cue did not reach backend")
}
worker := newPreferencesWorker(logger, settings, nil)
worker.sound = service
next := settings
test.change(&next)
worker.appSettings["Example Application"] = next
worker.switchApplication(context.Background(), "Example Application")
if worker.activeApp != "Example Application" || !reflect.DeepEqual(worker.settings, next) {
t.Fatal("application settings were not applied")
}
if got := playback.Err() == context.Canceled; got != test.cancel {
t.Fatalf("startup playback cancelled = %t, want %t", got, test.cancel)
}
})
}
}