After commit c018f5fd29 Fenrir is being switched over to go. If you do not want to be part of the testing process, please switch to master.

This commit is contained in:
Storm Dragon committed 2026-10-27 20:30:55 -04:00
1 parent c018f5fd29
commit 0e50de4c76
1047 files changed
+58364 -63747

No files matched your search

+209
View File
@@ -0,0 +1,209 @@
// Fenrir console screen reader.
// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
package sound
import (
"context"
"crypto/sha256"
"encoding/binary"
"fmt"
"math"
"slices"
"sync"
"testing"
"git.stormux.org/storm/fenrir/internal/presentation"
)
func TestProgressTonePitchDurationStereoAndVolume(t *testing.T) {
for _, percentage := range []float64{0, 25, 50, 75, 100} {
item := progress_tone(percentage)
frames := len(item.samples) / 2
if frames < sampleRate*40/1000 || frames > sampleRate*50/1000 {
t.Fatal("duration", frames)
}
crossings := 0
for frame := range frames {
left, right := item.samples[frame*2], item.samples[frame*2+1]
if left != right || math.Abs(float64(left)) > 7000.0/32768+0.00001 {
t.Fatal("stereo/amplitude", left, right)
}
if frame > 0 && item.samples[(frame-1)*2] <= 0 && left > 0 {
crossings++
}
}
gotHz := float64(crossings) * sampleRate / float64(frames)
wantHz := 110 * math.Pow(2, percentage/25)
if math.Abs(gotHz-wantHz) > 26 {
t.Fatal("pitch", percentage, gotHz, wantHz)
}
}
s := &Service{tones: progress_clips()}
s.Set_volume(.25)
s.Play_progress(50)
buffer := make([]float32, len(s.tones[50].samples))
s.read_samples(buffer)
for index, value := range buffer {
if value != s.tones[50].samples[index]*(float32(.25)*float32(1.0506)) {
t.Fatal("unscaled progress")
}
}
}
func TestProgressPriorityCoalescingAndCancellation(t *testing.T) {
s := &Service{clips: map[presentation.Cue]*clip{
presentation.Ready: {samples: []float32{1, 1, 2, 2}},
presentation.Top: {samples: []float32{3, 3, 4, 4}},
}, tones: [101]*clip{0: {samples: []float32{5, 5, 6, 6}}, 50: {samples: []float32{7, 7, 8, 8}}, 100: {samples: []float32{9, 9}}}}
s.Set_volume(1)
buffer := make([]float32, 2)
read := func(want float32) {
t.Helper()
s.read_samples(buffer)
if math.Abs(float64(buffer[0]-want)) > 0.00001 {
t.Fatal("wrong audio", buffer, want)
}
}
s.Play(presentation.Ready)
s.Play_progress(0)
read(1)
s.Play(presentation.Top)
s.Play_progress(50)
read(2)
read(3)
read(4)
read(7 * 1.0506)
s.Play_progress(0)
s.Play_progress(100)
read(8 * 1.0506)
read(9 * 1.0506)
read(0)
// Ordinary cues can interrupt a tone, and a pending tone survives that cue.
s.Play_progress(50)
read(7 * 1.0506)
s.Play_progress(100)
s.Play(presentation.Top)
read(3)
read(4)
read(9 * 1.0506)
s.Play_progress(50)
read(7 * 1.0506)
s.Play_progress(100)
s.Clear_progress()
read(0)
s.Play(presentation.Top)
read(3)
s.Clear_progress()
read(4)
// Shutdown stops progress admission and discards pending tones.
s.Play_progress(100)
s.Finish(context.Background())
read(0)
if s.Play_progress(50) {
t.Fatal("accepted during shutdown")
}
s.Close()
if s.Play_progress(50) {
t.Fatal("accepted after close")
}
}
func TestProgressAdmissionIsBoundedAndConcurrent(t *testing.T) {
s := &Service{tones: progress_clips()}
s.Set_volume(1)
for _, value := range []float64{-1, 101, math.NaN(), math.Inf(1)} {
if s.Play_progress(value) {
t.Fatal("invalid progress", value)
}
}
s.Play_progress(49.6)
if s.progress.Load() != s.tones[50] {
t.Fatal("fraction rounding")
}
cached := slices.Clone(s.tones[50].samples)
var workers sync.WaitGroup
workers.Go(func() {
for range 10000 {
s.Play_progress(50)
s.Discard_progress()
s.Clear_progress()
s.Play_progress(100)
}
})
buffer := make([]float32, 128)
for range 10000 {
s.read_samples(buffer)
}
workers.Wait()
if !slices.Equal(cached, s.tones[50].samples) {
t.Fatal("mutated cache")
}
if allocations := testing.AllocsPerRun(100, func() { s.Play_progress(50) }); allocations != 0 {
t.Fatal("admission allocates", allocations)
}
if allocations := testing.AllocsPerRun(100, func() { s.read_samples(buffer) }); allocations != 0 {
t.Fatal("callback allocates", allocations)
}
}
func TestDiscardProgressPreservesEntirePlayingTone(t *testing.T) {
s := &Service{tones: progress_clips()}
s.Set_volume(1)
s.Play_progress(50)
tone := s.tones[50].samples
split := len(tone) / 4 * 2 // Keep stereo frames paired.
first := make([]float32, split)
s.read_samples(first)
s.Play_progress(100)
s.Discard_progress()
tail := make([]float32, len(tone)-split)
s.read_samples(tail)
for index, value := range append(first, tail...) {
if value != tone[index]*float32(progressGain) {
t.Fatalf("tone cut short or altered at sample %d: got %v, want %v", index, value, tone[index]*float32(progressGain))
}
}
s.read_samples(tail)
for _, value := range tail {
if value != 0 {
t.Fatal("discarded waiting tone still played")
}
}
// A reset must not prevent the next job from making progress sounds.
s.Play_progress(100)
s.read_samples(tail)
if !slices.ContainsFunc(tail, func(value float32) bool { return value != 0 }) {
t.Fatal("new progress tone was lost after discard")
}
}
func TestProgressSamplesMatchCthulhuReference(t *testing.T) {
// Digest of Cthulhu's unmodified progress_tone_wav for integer 0..100,
// independently resampled from 44100 Hz PCM16 to our 48000 Hz stereo float.
hash := sha256.New()
for _, item := range progress_clips() {
for _, sample := range item.samples {
if err := binary.Write(hash, binary.LittleEndian, sample); err != nil {
t.Fatal(err)
}
}
}
if got := fmt.Sprintf("%x", hash.Sum(nil)); got != "b6e0adff86b6659465ff6942c4651b84e76d855ba6a8b7e3a982a88f8dde4e9b" {
t.Fatal("Cthulhu tone samples differ", got)
}
}