Files
skald/diskwriter/diskwriter_test.go
2026-05-24 00:46:14 -04:00

334 lines
8.7 KiB
Go

package diskwriter
import (
"encoding/binary"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"git.stormux.org/storm/skald/conn"
"git.stormux.org/storm/skald/hall"
)
func TestSanitise(t *testing.T) {
tests := []struct{ a, b string }{
{"Alas", "Alas"},
{", poor Horatio", ", poor Horatio"},
{"I/knew\\him/well", "I-slash-knew-backslash-him-slash-well"},
}
for _, tt := range tests {
c := sanitise(tt.a)
if c != tt.b {
t.Errorf("sanitise(%v): got %v, expected %v",
tt.a, c, tt.b)
}
}
}
func TestOpenDiskFileUsesSkaldOggName(t *testing.T) {
dir := t.TempDir()
f, err := openDiskFile(dir, "ogg")
if err != nil {
t.Fatal(err)
}
name := filepath.Base(f.Name())
f.Close()
if !strings.HasPrefix(name, "skald-recording-") {
t.Fatalf("filename %q does not use Skald recording prefix", name)
}
if !strings.HasSuffix(name, ".ogg") {
t.Fatalf("filename %q does not use Ogg extension", name)
}
if _, err := os.Stat(filepath.Join(dir, name)); err != nil {
t.Fatal(err)
}
}
func TestCleanupOldRecordings(t *testing.T) {
oldDirectory := Directory
defer func() {
Directory = oldDirectory
}()
Directory = t.TempDir()
hallDir := filepath.Join(Directory, "test")
if err := os.MkdirAll(hallDir, 0700); err != nil {
t.Fatal(err)
}
oldRecording := filepath.Join(hallDir, "skald-recording-20000101-000000.ogg")
activeRecording := filepath.Join(hallDir, "skald-recording-20000101-010000.ogg")
newRecording := filepath.Join(hallDir, "skald-recording-29990101-000000.ogg")
otherFile := filepath.Join(hallDir, "notes.txt")
for _, name := range []string{oldRecording, activeRecording, newRecording, otherFile} {
if err := os.WriteFile(name, []byte("test"), 0600); err != nil {
t.Fatal(err)
}
}
oldTime := time.Now().Add(-48 * time.Hour)
newTime := time.Now()
if err := os.Chtimes(oldRecording, oldTime, oldTime); err != nil {
t.Fatal(err)
}
if err := os.Chtimes(activeRecording, oldTime, oldTime); err != nil {
t.Fatal(err)
}
if err := os.Chtimes(newRecording, newTime, newTime); err != nil {
t.Fatal(err)
}
if err := os.Chtimes(otherFile, oldTime, oldTime); err != nil {
t.Fatal(err)
}
activeRecordings.Store(activeRecording, struct{}{})
defer activeRecordings.Delete(activeRecording)
if err := CleanupOldRecordings(24 * time.Hour); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(oldRecording); !os.IsNotExist(err) {
t.Fatalf("old recording still exists or stat failed unexpectedly: %v", err)
}
if _, err := os.Stat(newRecording); err != nil {
t.Fatalf("new recording was removed: %v", err)
}
if _, err := os.Stat(activeRecording); err != nil {
t.Fatalf("active recording was removed: %v", err)
}
if _, err := os.Stat(otherFile); err != nil {
t.Fatalf("non-recording file was removed: %v", err)
}
}
func TestMixFrameAppliesHeadroomForMultipleSources(t *testing.T) {
a := make(chan []byte, 1)
b := make(chan []byte, 1)
a <- pcmFrame(20000)
b <- pcmFrame(20000)
out := mixFrame([]chan []byte{a, b})
if len(out) != bytesPerFrame {
t.Fatalf("mixed frame has length %d, expected %d", len(out), bytesPerFrame)
}
for i := 0; i < len(out); i += 2 {
got := int16(binary.LittleEndian.Uint16(out[i:]))
if got != 28071 {
t.Fatalf("sample %d = %d, expected headroom-adjusted 28071", i/2, got)
}
}
}
func TestMixFrameLimiterPreservesNormalSpeechLevel(t *testing.T) {
a := make(chan []byte, 1)
a <- pcmFrame(12000)
out := mixFrame([]chan []byte{a})
for i := 0; i < len(out); i += 2 {
got := int16(binary.LittleEndian.Uint16(out[i:]))
if got != 12000 {
t.Fatalf("sample %d = %d, expected unchanged 12000", i/2, got)
}
}
}
func TestMixFrameHandlesQuietAndLoudSources(t *testing.T) {
a := make(chan []byte, 1)
b := make(chan []byte, 1)
a <- pcmFrame(1000)
b <- pcmFrame(20000)
out := mixFrame([]chan []byte{a, b})
for i := 0; i < len(out); i += 2 {
got := int16(binary.LittleEndian.Uint16(out[i:]))
if got != 14849 {
t.Fatalf("sample %d = %d, expected mixed quiet/loud sample 14849", i/2, got)
}
}
}
func TestMixFrameDoesNotAttenuateSpeakerForSilentOpenMics(t *testing.T) {
speaker := make(chan []byte, 1)
silent1 := make(chan []byte, 1)
silent2 := make(chan []byte, 1)
speaker <- pcmFrame(12000)
silent1 <- pcmFrame(0)
silent2 <- pcmFrame(0)
out := mixFrame([]chan []byte{speaker, silent1, silent2})
for i := 0; i < len(out); i += 2 {
got := int16(binary.LittleEndian.Uint16(out[i:]))
if got != 12000 {
t.Fatalf("sample %d = %d, expected unattenuated speaker 12000", i/2, got)
}
}
}
func TestMixFrameLimiterBoundsOverloadedSamples(t *testing.T) {
a := make(chan []byte, 1)
b := make(chan []byte, 1)
a <- pcmFrame(32000)
b <- pcmFrame(32000)
out := mixFrame([]chan []byte{a, b})
for i := 0; i < len(out); i += 2 {
got := int16(binary.LittleEndian.Uint16(out[i:]))
if got <= 0 || got > 32767 {
t.Fatalf("sample %d = %d, expected bounded positive sample", i/2, got)
}
if got >= 32767 {
t.Fatalf("sample %d = %d, expected limiter below hard clip", i/2, got)
}
}
}
func TestMixFrameConsumesOldestQueuedFrame(t *testing.T) {
source := make(chan []byte, 2)
source <- pcmFrame(1000)
source <- pcmFrame(2000)
out := mixFrame([]chan []byte{source})
if len(out) != bytesPerFrame {
t.Fatalf("mixed frame has length %d, expected %d", len(out), bytesPerFrame)
}
for i := 0; i < len(out); i += 2 {
got := int16(binary.LittleEndian.Uint16(out[i:]))
if got != 1000 {
t.Fatalf("sample %d = %d, expected oldest queued sample 1000", i/2, got)
}
}
if queued := len(source); queued != 1 {
t.Fatalf("queued frames after mix = %d, expected 1", queued)
}
}
func TestMixFrameReturnsNilForSilence(t *testing.T) {
if out := mixFrame(nil); out != nil {
t.Fatalf("expected nil silent frame, got %d bytes", len(out))
}
}
func TestNewDiskConnIgnoresConnectionWithoutRecordableTracks(t *testing.T) {
down, err := newDiskConn(&Client{}, &fakeUp{id: "up1"}, nil)
if err != nil {
t.Fatal(err)
}
if down != nil {
t.Fatalf("expected no disk connection for no tracks, got %#v", down)
}
}
func TestPushConnDoesNotStoreConnectionWithoutRecordableTracks(t *testing.T) {
g := &hall.Hall{}
client := &Client{
hall: g,
recorder: &recorder{},
down: make(map[string]*diskConn),
}
err := client.PushConn(g, "up1", &fakeUp{id: "up1"}, nil, "")
if err != nil {
t.Fatal(err)
}
if len(client.down) != 0 {
t.Fatalf("stored %d down connections, expected 0", len(client.down))
}
}
func TestRecorderCreatesPlayableOgg(t *testing.T) {
if _, err := exec.LookPath("ffmpeg"); err != nil {
t.Skip("ffmpeg not installed")
}
if _, err := exec.LookPath("ffprobe"); err != nil {
t.Skip("ffprobe not installed")
}
r, err := newRecorder(nil, t.TempDir())
if err != nil {
t.Fatal(err)
}
time.Sleep(100 * time.Millisecond)
if err := r.Close(); err != nil {
t.Fatal(err)
}
cmd := exec.Command(
"ffprobe",
"-v", "error",
"-select_streams", "a:0",
"-show_entries", "stream=codec_name",
"-of", "default=noprint_wrappers=1:nokey=1",
r.path,
)
out, err := cmd.Output()
if err != nil {
t.Fatal(err)
}
if strings.TrimSpace(string(out)) != "opus" {
t.Fatalf("expected opus stream, got %q", strings.TrimSpace(string(out)))
}
}
func TestSyntheticRecordingCreatesPlayableOgg(t *testing.T) {
if _, err := exec.LookPath("ffmpeg"); err != nil {
t.Skip("ffmpeg not installed")
}
if _, err := exec.LookPath("ffprobe"); err != nil {
t.Skip("ffprobe not installed")
}
result, err := RunSyntheticRecording(t.TempDir(), 3, 100*time.Millisecond)
if err != nil {
t.Fatal(err)
}
if result.Metrics.DecoderStarts != 3 {
t.Fatalf("decoder starts = %d, expected 3", result.Metrics.DecoderStarts)
}
if result.Metrics.DecoderStops != 3 {
t.Fatalf("decoder stops = %d, expected 3", result.Metrics.DecoderStops)
}
if result.Metrics.DroppedMixedFrames != 0 {
t.Fatalf("dropped mixed frames = %d, expected 0", result.Metrics.DroppedMixedFrames)
}
cmd := exec.Command(
"ffprobe",
"-v", "error",
"-select_streams", "a:0",
"-show_entries", "stream=codec_name",
"-of", "default=noprint_wrappers=1:nokey=1",
result.Path,
)
out, err := cmd.Output()
if err != nil {
t.Fatal(err)
}
if strings.TrimSpace(string(out)) != "opus" {
t.Fatalf("expected opus stream, got %q", strings.TrimSpace(string(out)))
}
}
func pcmFrame(sample int16) []byte {
out := make([]byte, bytesPerFrame)
for i := 0; i < len(out); i += 2 {
binary.LittleEndian.PutUint16(out[i:], uint16(sample))
}
return out
}
type fakeUp struct {
id string
}
func (u *fakeUp) AddLocal(conn.Down) error { return nil }
func (u *fakeUp) DelLocal(conn.Down) bool { return true }
func (u *fakeUp) Id() string { return u.id }
func (u *fakeUp) Label() string { return "audio" }
func (u *fakeUp) User() (string, string) { return "user-id", "Username" }