|
- package rating
-
- import (
- "bytes"
- "os"
- "testing"
- )
-
- // buildMinimalMP3 returns a minimal valid MP3: a 10-byte ID3v2.3 header with
- // no frames (tag size = 0) followed by 4096 bytes of fake audio data (0xFF 0xFB
- // sync word repeated, which is close enough for our purposes).
- func buildMinimalMP3() []byte {
- var buf bytes.Buffer
- // ID3v2.3 header: "ID3" + version 2.3.0 + flags 0 + synchsafe size 0
- buf.Write([]byte{'I', 'D', '3', 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
- // Fake audio data
- audio := make([]byte, 4096)
- for i := range audio {
- audio[i] = byte(i & 0xFF)
- }
- buf.Write(audio)
- return buf.Bytes()
- }
-
- func writeTempMP3(t *testing.T, data []byte) string {
- t.Helper()
- f, err := os.CreateTemp(t.TempDir(), "test-*.mp3")
- if err != nil {
- t.Fatalf("create temp: %v", err)
- }
- if _, err := f.Write(data); err != nil {
- t.Fatalf("write temp: %v", err)
- }
- f.Close()
- return f.Name()
- }
-
- func TestPopmToStars(t *testing.T) {
- cases := []struct{ in uint8; want int }{
- {0, 0}, {1, 1}, {31, 1}, {32, 2}, {95, 2},
- {96, 3}, {127, 3}, {128, 3}, {159, 3},
- {160, 4}, {195, 4}, {196, 4}, {223, 4},
- {224, 5}, {255, 5},
- }
- for _, c := range cases {
- got := popmToStars(c.in)
- if got != c.want {
- t.Errorf("popmToStars(%d) = %d, want %d", c.in, got, c.want)
- }
- }
- }
-
- func TestStarsToPOPM(t *testing.T) {
- want := [6]uint8{0, 1, 64, 128, 196, 255}
- for i, w := range want {
- got := starsToPOPM(i)
- if got != w {
- t.Errorf("starsToPOPM(%d) = %d, want %d", i, got, w)
- }
- }
- }
-
- func TestId3v2AudioStart_noTag(t *testing.T) {
- // File with no ID3 header
- f, err := os.CreateTemp(t.TempDir(), "noTag-*.mp3")
- if err != nil {
- t.Fatal(err)
- }
- f.Write([]byte{0xFF, 0xFB, 0x90, 0x00}) // MP3 sync
- f.Close()
-
- start, err := id3v2AudioStart(f.Name())
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if start != 0 {
- t.Errorf("got audioStart=%d, want 0", start)
- }
- }
-
- func TestId3v2AudioStart_withHeader(t *testing.T) {
- // Header with tag size 0 → audioStart should be 10
- data := buildMinimalMP3()
- path := writeTempMP3(t, data)
-
- start, err := id3v2AudioStart(path)
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if start != 10 {
- t.Errorf("got audioStart=%d, want 10", start)
- }
- }
-
- func TestSetAndGet_roundtrip(t *testing.T) {
- original := buildMinimalMP3()
- path := writeTempMP3(t, original)
-
- // File should start with no rating
- r, err := Get(path)
- if err != nil {
- t.Fatalf("Get before Set: %v", err)
- }
- if r != 0 {
- t.Errorf("expected 0 before Set, got %d", r)
- }
-
- // Set each star value and read back
- for stars := 1; stars <= 5; stars++ {
- if err := Set(path, stars); err != nil {
- t.Fatalf("Set(%d): %v", stars, err)
- }
-
- // File must not be truncated — must be larger than tag-only (> original size)
- info, err := os.Stat(path)
- if err != nil {
- t.Fatalf("stat after Set(%d): %v", stars, err)
- }
- if info.Size() < int64(len(original)) {
- t.Errorf("Set(%d) shrunk file: got %d bytes, original was %d",
- stars, info.Size(), len(original))
- }
-
- got, err := Get(path)
- if err != nil {
- t.Fatalf("Get after Set(%d): %v", stars, err)
- }
- if got != stars {
- t.Errorf("Get after Set(%d) = %d, want %d", stars, got, stars)
- }
- }
-
- // Unrate (stars=0) must remove POPM
- if err := Set(path, 0); err != nil {
- t.Fatalf("Set(0): %v", err)
- }
- got, err := Get(path)
- if err != nil {
- t.Fatalf("Get after Set(0): %v", err)
- }
- if got != 0 {
- t.Errorf("Get after Set(0) = %d, want 0", got)
- }
-
- // Audio data must be intact after all the Set calls
- data, err := os.ReadFile(path)
- if err != nil {
- t.Fatalf("ReadFile: %v", err)
- }
- audioStart, _ := id3v2AudioStart(path)
- if audioStart >= int64(len(data)) {
- t.Fatalf("audioStart %d >= fileSize %d", audioStart, len(data))
- }
- got4096 := data[audioStart:]
- orig4096 := original[10:] // original audio starts at byte 10 (tag size 0)
- if len(got4096) < len(orig4096) {
- t.Errorf("audio data truncated: got %d bytes, want %d", len(got4096), len(orig4096))
- } else {
- // Last 4096 bytes should match original audio
- tail := got4096[len(got4096)-len(orig4096):]
- if !bytes.Equal(tail, orig4096) {
- t.Error("audio data corrupted after rating Set calls")
- }
- }
- }
-
- func TestSet_invalidStars(t *testing.T) {
- path := writeTempMP3(t, buildMinimalMP3())
- if err := Set(path, 6); err == nil {
- t.Error("Set(6) should return error")
- }
- if err := Set(path, -1); err == nil {
- t.Error("Set(-1) should return error")
- }
- }
-
- func TestSet_notMP3(t *testing.T) {
- f, err := os.CreateTemp(t.TempDir(), "test-*.flac")
- if err != nil {
- t.Fatal(err)
- }
- f.Close()
- if err := Set(f.Name(), 3); err == nil {
- t.Error("Set on non-MP3 should return error")
- }
- }
|