test(16-02): add FLAC writer round-trip tests with 7 test functions
- TestWriteFlacTags_TextFields: round-trip 9 text fields via metadata.ExtractTags - TestWriteFlacTags_CoverArt: JPEG embed and read-back verification - TestWriteFlacTags_ClearCoverArt: embed then clear art, verify removal - TestWriteFlacTags_PartialUpdate: update 2 of 8 fields, verify others preserved - TestWriteFlacTags_PreservesStreamInfo: verify StreamInfo block survives tag writes - TestWriteFlacTags_ReplaceComment: write twice, verify no duplicate Vorbis Comments - TestWriteFlacTags_AtomicSafety: verify corrupt/invalid writes leave original intact - Create shared helpers_test.go with testLogger, tinyJPEG, assertEqual, etc.
This commit is contained in:
@@ -0,0 +1,466 @@
|
|||||||
|
package tagwriter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
flac "github.com/go-flac/go-flac/v2"
|
||||||
|
|
||||||
|
"yellowjacket/backend/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test-only sentinel errors for the Vorbis Comment parser.
|
||||||
|
var (
|
||||||
|
errVorbisCommentTooShort = errors.New("vorbis comment too short")
|
||||||
|
errVorbisCommentTruncated = errors.New("vorbis comment truncated")
|
||||||
|
)
|
||||||
|
|
||||||
|
// makeMinimalFLAC creates a minimal valid FLAC file at path.
|
||||||
|
// The file contains: fLaC header, StreamInfo metadata block, and a
|
||||||
|
// minimal valid FLAC frame (sync code + padding).
|
||||||
|
func makeMinimalFLAC(t *testing.T, path string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
// fLaC magic.
|
||||||
|
buf.WriteString("fLaC")
|
||||||
|
|
||||||
|
// StreamInfo metadata block (type=0, last=true → 0x80).
|
||||||
|
// StreamInfo data is exactly 34 bytes.
|
||||||
|
streamInfo := makeStreamInfoData()
|
||||||
|
// Block header: type byte (0x80 = last + StreamInfo), 3-byte length.
|
||||||
|
buf.WriteByte(0x80) // last-metadata-block flag | StreamInfo type
|
||||||
|
// 3-byte big-endian length of StreamInfo data.
|
||||||
|
siLen := uint32(len(streamInfo))
|
||||||
|
buf.WriteByte(byte(siLen >> 16))
|
||||||
|
buf.WriteByte(byte(siLen >> 8))
|
||||||
|
buf.WriteByte(byte(siLen))
|
||||||
|
buf.Write(streamInfo)
|
||||||
|
|
||||||
|
// Minimal FLAC frame — sync code 0xFFF8 + enough padding bytes to
|
||||||
|
// form a parseable (though silent) frame. The go-flac library only
|
||||||
|
// checks for the sync code (0xFF, byte>>2 == 0x3E) when parsing frames.
|
||||||
|
buf.Write([]byte{0xFF, 0xF8}) // sync code
|
||||||
|
// Add some zero padding as a pseudo-frame body.
|
||||||
|
buf.Write(make([]byte, 16))
|
||||||
|
|
||||||
|
if err := os.WriteFile(path, buf.Bytes(), 0o644); err != nil {
|
||||||
|
t.Fatalf("write minimal FLAC: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeStreamInfoData creates a 34-byte StreamInfo metadata block body:
|
||||||
|
// - min/max block size: 4096
|
||||||
|
// - min/max frame size: 0
|
||||||
|
// - sample rate: 44100, channels: 1 (mono), bits per sample: 16
|
||||||
|
// - total samples: 0
|
||||||
|
// - MD5: all zeros.
|
||||||
|
func makeStreamInfoData() []byte {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
_ = binary.Write(&buf, binary.BigEndian, uint16(4096)) // min block size
|
||||||
|
_ = binary.Write(&buf, binary.BigEndian, uint16(4096)) // max block size
|
||||||
|
|
||||||
|
// min frame size (3 bytes, big-endian).
|
||||||
|
buf.Write([]byte{0, 0, 0})
|
||||||
|
// max frame size (3 bytes, big-endian).
|
||||||
|
buf.Write([]byte{0, 0, 0})
|
||||||
|
|
||||||
|
// Next 8 bytes pack: sample rate (20 bits), channels-1 (3 bits),
|
||||||
|
// bits-per-sample-1 (5 bits), total samples (36 bits).
|
||||||
|
//
|
||||||
|
// 44100 Hz, 1 channel, 16 bps, 0 total samples.
|
||||||
|
buf.Write([]byte{
|
||||||
|
0xAC, 0x44, 0x00, 0xF0,
|
||||||
|
0x00, 0x00, 0x00, 0x00,
|
||||||
|
})
|
||||||
|
|
||||||
|
// MD5 signature (16 bytes of zeros).
|
||||||
|
buf.Write(make([]byte, 16))
|
||||||
|
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteFlacTags_TextFields(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
flacPath := filepath.Join(tmpDir, "test.flac")
|
||||||
|
makeMinimalFLAC(t, flacPath)
|
||||||
|
|
||||||
|
changes := TagChanges{
|
||||||
|
FieldTitle: "Test Title",
|
||||||
|
FieldArtist: "Test Artist",
|
||||||
|
FieldAlbum: "Test Album",
|
||||||
|
FieldAlbumArtist: "Test Album Artist",
|
||||||
|
FieldGenre: "Rock",
|
||||||
|
FieldYear: 2024,
|
||||||
|
FieldTrackNumber: 3,
|
||||||
|
FieldDiscNumber: 1,
|
||||||
|
FieldComposer: "Test Composer",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeFlacTags(testLogger(), flacPath, changes); err != nil {
|
||||||
|
t.Fatalf("writeFlacTags: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read back with metadata.ExtractTags to verify round-trip.
|
||||||
|
tags, err := metadata.ExtractTags(flacPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ExtractTags: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEqual(t, "Title", "Test Title", tags.Title)
|
||||||
|
assertEqual(t, "Artist", "Test Artist", tags.Artist)
|
||||||
|
assertEqual(t, "Album", "Test Album", tags.Album)
|
||||||
|
assertEqual(t, "AlbumArtist", "Test Album Artist", tags.AlbumArtist)
|
||||||
|
assertEqual(t, "Genre", "Rock", tags.Genre)
|
||||||
|
assertEqual(t, "Composer", "Test Composer", tags.Composer)
|
||||||
|
assertEqual(t, "Year", 2024, tags.Year)
|
||||||
|
assertEqual(t, "TrackNumber", 3, tags.TrackNumber)
|
||||||
|
assertEqual(t, "DiscNumber", 1, tags.DiscNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteFlacTags_CoverArt(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
flacPath := filepath.Join(tmpDir, "test.flac")
|
||||||
|
makeMinimalFLAC(t, flacPath)
|
||||||
|
|
||||||
|
jpegData := tinyJPEG(t)
|
||||||
|
|
||||||
|
changes := TagChanges{
|
||||||
|
FieldCoverArt: jpegData,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeFlacTags(testLogger(), flacPath, changes); err != nil {
|
||||||
|
t.Fatalf("writeFlacTags: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tags, err := metadata.ExtractTags(flacPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ExtractTags: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tags.Picture == nil {
|
||||||
|
t.Fatal("expected picture data, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(tags.Picture.Data, jpegData) {
|
||||||
|
t.Errorf("picture data mismatch: got %d bytes, want %d bytes",
|
||||||
|
len(tags.Picture.Data), len(jpegData))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEqual(t, "MIME type", "image/jpeg", tags.Picture.MIMEType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteFlacTags_ClearCoverArt(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
flacPath := filepath.Join(tmpDir, "test.flac")
|
||||||
|
makeMinimalFLAC(t, flacPath)
|
||||||
|
|
||||||
|
// First add cover art.
|
||||||
|
jpegData := tinyJPEG(t)
|
||||||
|
|
||||||
|
err := writeFlacTags(testLogger(), flacPath, TagChanges{FieldCoverArt: jpegData})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("add cover art: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify art was added.
|
||||||
|
tags, err := metadata.ExtractTags(flacPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ExtractTags after add: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tags.Picture == nil {
|
||||||
|
t.Fatal("expected picture after add, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now clear cover art (nil value).
|
||||||
|
if err := writeFlacTags(testLogger(), flacPath, TagChanges{FieldCoverArt: nil}); err != nil {
|
||||||
|
t.Fatalf("clear cover art: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tags, err = metadata.ExtractTags(flacPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ExtractTags after clear: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tags.Picture != nil {
|
||||||
|
t.Errorf("expected no picture after clear, got %d bytes", len(tags.Picture.Data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteFlacTags_PartialUpdate(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
flacPath := filepath.Join(tmpDir, "test.flac")
|
||||||
|
makeMinimalFLAC(t, flacPath)
|
||||||
|
|
||||||
|
// Write all fields first.
|
||||||
|
allFields := TagChanges{
|
||||||
|
FieldTitle: "Original Title",
|
||||||
|
FieldArtist: "Original Artist",
|
||||||
|
FieldAlbum: "Original Album",
|
||||||
|
FieldGenre: "Jazz",
|
||||||
|
FieldYear: 2020,
|
||||||
|
FieldTrackNumber: 5,
|
||||||
|
FieldDiscNumber: 2,
|
||||||
|
FieldComposer: "Original Composer",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeFlacTags(testLogger(), flacPath, allFields); err != nil {
|
||||||
|
t.Fatalf("write all fields: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update only title and genre.
|
||||||
|
partial := TagChanges{
|
||||||
|
FieldTitle: "Updated Title",
|
||||||
|
FieldGenre: "Blues",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeFlacTags(testLogger(), flacPath, partial); err != nil {
|
||||||
|
t.Fatalf("partial update: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tags, err := metadata.ExtractTags(flacPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ExtractTags: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changed fields should have new values.
|
||||||
|
assertEqual(t, "Title", "Updated Title", tags.Title)
|
||||||
|
assertEqual(t, "Genre", "Blues", tags.Genre)
|
||||||
|
|
||||||
|
// Unchanged fields should be preserved.
|
||||||
|
assertEqual(t, "Artist", "Original Artist", tags.Artist)
|
||||||
|
assertEqual(t, "Album", "Original Album", tags.Album)
|
||||||
|
assertEqual(t, "Year", 2020, tags.Year)
|
||||||
|
assertEqual(t, "TrackNumber", 5, tags.TrackNumber)
|
||||||
|
assertEqual(t, "DiscNumber", 2, tags.DiscNumber)
|
||||||
|
assertEqual(t, "Composer", "Original Composer", tags.Composer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteFlacTags_PreservesStreamInfo(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
flacPath := filepath.Join(tmpDir, "test.flac")
|
||||||
|
makeMinimalFLAC(t, flacPath)
|
||||||
|
|
||||||
|
// Parse before writing to capture original StreamInfo.
|
||||||
|
fBefore, err := flac.ParseFile(flacPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse before: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
siBefore, err := fBefore.GetStreamInfo()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get stream info before: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = fBefore.Close()
|
||||||
|
|
||||||
|
// Write tags.
|
||||||
|
changes := TagChanges{
|
||||||
|
FieldTitle: "Some Title",
|
||||||
|
FieldArtist: "Some Artist",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeFlacTags(testLogger(), flacPath, changes); err != nil {
|
||||||
|
t.Fatalf("writeFlacTags: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse after writing and verify StreamInfo is intact.
|
||||||
|
fAfter, err := flac.ParseFile(flacPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse after: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = fAfter.Close() }()
|
||||||
|
|
||||||
|
siAfter, err := fAfter.GetStreamInfo()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get stream info after: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEqual(t, "SampleRate", siBefore.SampleRate, siAfter.SampleRate)
|
||||||
|
assertEqual(t, "ChannelCount", siBefore.ChannelCount, siAfter.ChannelCount)
|
||||||
|
assertEqual(t, "BitDepth", siBefore.BitDepth, siAfter.BitDepth)
|
||||||
|
assertEqual(t, "BlockSizeMin", siBefore.BlockSizeMin, siAfter.BlockSizeMin)
|
||||||
|
|
||||||
|
// Verify the file has StreamInfo as the first metadata block.
|
||||||
|
if fAfter.Meta[0].Type != flac.StreamInfo {
|
||||||
|
t.Errorf("first metadata block type: got %d, want %d (StreamInfo)",
|
||||||
|
fAfter.Meta[0].Type, flac.StreamInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteFlacTags_ReplaceComment(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
flacPath := filepath.Join(tmpDir, "test.flac")
|
||||||
|
makeMinimalFLAC(t, flacPath)
|
||||||
|
|
||||||
|
// Write title once.
|
||||||
|
if err := writeFlacTags(testLogger(), flacPath, TagChanges{FieldTitle: "First"}); err != nil {
|
||||||
|
t.Fatalf("first write: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write title again.
|
||||||
|
if err := writeFlacTags(testLogger(), flacPath, TagChanges{FieldTitle: "Second"}); err != nil {
|
||||||
|
t.Fatalf("second write: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the FLAC and check no duplicate TITLE entries.
|
||||||
|
f, err := flac.ParseFile(flacPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = f.Close() }()
|
||||||
|
|
||||||
|
for _, meta := range f.Meta {
|
||||||
|
if meta.Type != flac.VorbisComment {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cmt, parseErr := parseVorbisComment(*meta)
|
||||||
|
if parseErr != nil {
|
||||||
|
t.Fatalf("parse vorbis comments: %v", parseErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
titles := cmt.get("TITLE")
|
||||||
|
if len(titles) != 1 {
|
||||||
|
t.Errorf("expected 1 TITLE entry, got %d: %v", len(titles), titles)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(titles) > 0 && titles[0] != "Second" {
|
||||||
|
t.Errorf("TITLE value: got %q, want %q", titles[0], "Second")
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also verify via metadata.ExtractTags round-trip.
|
||||||
|
tags, err := metadata.ExtractTags(flacPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ExtractTags: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEqual(t, "Title", "Second", tags.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteFlacTags_AtomicSafety(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
// Verify that writing to a corrupt (non-FLAC) file fails and
|
||||||
|
// leaves the file untouched.
|
||||||
|
corruptPath := filepath.Join(tmpDir, "corrupt.flac")
|
||||||
|
if err := os.WriteFile(corruptPath, []byte("not a flac file"), 0o644); err != nil {
|
||||||
|
t.Fatalf("write corrupt: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := writeFlacTags(testLogger(), corruptPath, TagChanges{FieldTitle: "Fail"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error writing to corrupt file, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The corrupt file should remain untouched.
|
||||||
|
content, readErr := os.ReadFile(corruptPath)
|
||||||
|
if readErr != nil {
|
||||||
|
t.Fatalf("read corrupt file: %v", readErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(content) != "not a flac file" {
|
||||||
|
t.Errorf("corrupt file was modified: got %q", string(content))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that a valid FLAC file stays intact when the write fails
|
||||||
|
// due to invalid cover art data.
|
||||||
|
safePath := filepath.Join(tmpDir, "safe.flac")
|
||||||
|
makeMinimalFLAC(t, safePath)
|
||||||
|
|
||||||
|
safeBefore, readErr := os.ReadFile(safePath)
|
||||||
|
if readErr != nil {
|
||||||
|
t.Fatalf("read safe before: %v", readErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide random bytes that aren't valid JPEG or PNG — this will
|
||||||
|
// cause flacpicture.NewFromImageData to fail, which happens before
|
||||||
|
// AtomicWrite so the original file stays intact.
|
||||||
|
randomData := make([]byte, 32)
|
||||||
|
_, _ = rand.Read(randomData)
|
||||||
|
|
||||||
|
err = writeFlacTags(testLogger(), safePath, TagChanges{FieldCoverArt: randomData})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error with invalid image data, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
safeAfter, readErr := os.ReadFile(safePath)
|
||||||
|
if readErr != nil {
|
||||||
|
t.Fatalf("read safe after: %v", readErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(safeBefore, safeAfter) {
|
||||||
|
t.Error("safe file was modified despite failed write")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseVorbisComment is a test helper to parse Vorbis Comments from a raw
|
||||||
|
// metadata block using a minimal inline parser.
|
||||||
|
func parseVorbisComment(meta flac.MetaDataBlock) (*vorbisCommentHelper, error) {
|
||||||
|
result := &vorbisCommentHelper{comments: map[string][]string{}}
|
||||||
|
data := meta.Data
|
||||||
|
|
||||||
|
if len(data) < 4 { //nolint:mnd
|
||||||
|
return nil, errVorbisCommentTooShort
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vendor string length (little-endian 32-bit).
|
||||||
|
vendorLen := int(binary.LittleEndian.Uint32(data[0:4]))
|
||||||
|
offset := 4 + vendorLen
|
||||||
|
|
||||||
|
if offset+4 > len(data) {
|
||||||
|
return nil, fmt.Errorf("%w: after vendor string", errVorbisCommentTruncated)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comment count.
|
||||||
|
count := int(binary.LittleEndian.Uint32(data[offset : offset+4]))
|
||||||
|
offset += 4
|
||||||
|
|
||||||
|
for range count {
|
||||||
|
if offset+4 > len(data) {
|
||||||
|
return nil, fmt.Errorf("%w: comment header", errVorbisCommentTruncated)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmtLen := int(binary.LittleEndian.Uint32(data[offset : offset+4]))
|
||||||
|
offset += 4
|
||||||
|
|
||||||
|
if offset+cmtLen > len(data) {
|
||||||
|
return nil, fmt.Errorf("%w: comment entry", errVorbisCommentTruncated)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := string(data[offset : offset+cmtLen])
|
||||||
|
offset += cmtLen
|
||||||
|
|
||||||
|
key, val, found := strings.Cut(entry, "=")
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result.comments[key] = append(result.comments[key], val)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type vorbisCommentHelper struct {
|
||||||
|
comments map[string][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *vorbisCommentHelper) get(key string) []string {
|
||||||
|
return v.comments[key]
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package tagwriter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/jpeg"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// testLogger returns a quiet logger suitable for tests.
|
||||||
|
func testLogger() *slog.Logger {
|
||||||
|
return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||||
|
Level: slog.LevelError,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// tinyJPEG returns a small, valid JPEG image for cover art tests.
|
||||||
|
func tinyJPEG(t *testing.T) []byte {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
img := image.NewRGBA(image.Rect(0, 0, 1, 1))
|
||||||
|
img.Set(0, 0, color.RGBA{R: 255, A: 255})
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := jpeg.Encode(&buf, img, nil); err != nil {
|
||||||
|
t.Fatalf("encode tiny jpeg: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeMinimalJPEG is an alias for tinyJPEG used by mp3_test.go.
|
||||||
|
func makeMinimalJPEG(t *testing.T) []byte {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
return tinyJPEG(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// assertEqual is a generic test helper for comparing values.
|
||||||
|
func assertEqual[T comparable](t *testing.T, field string, want, got T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("%s: got %v, want %v", field, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertStrField(t *testing.T, name, got, want string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("%s: got %q, want %q", name, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertIntField(t *testing.T, name string, got, want int) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("%s: got %d, want %d", name, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user