mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
CI runs the suite under -race, which the local `make test` does not, so this only showed up there: 230 specs passed and the detector still failed the run. The drain-pools spec started Run and never waited for it, so pool goroutines outlived the spec and raced the config snapshot Ginkgo restores on cleanup. It now cancels, unparks the blocked lookups and waits for Run to return. Two test doubles also had to become concurrency-safe, since the spec is the first to resolve several artists at once: fakeImageAgent's call counters, and MockAlbumRepo.GetAll, which records the last query options on a read path. MockDataStore's lazy accessors get the same treatment — only MediaFile was guarded before, and two pools now reach them concurrently. ArtworkQueue takes an unlocked helper for its internal Artwork call, since repoMu is not reentrant.
370 lines
9.7 KiB
Go
370 lines
9.7 KiB
Go
package tests
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
|
|
"github.com/navidrome/navidrome/model"
|
|
)
|
|
|
|
type MockDataStore struct {
|
|
RealDS model.DataStore
|
|
MockedLibrary model.LibraryRepository
|
|
MockedFolder model.FolderRepository
|
|
MockedGenre model.GenreRepository
|
|
MockedAlbum model.AlbumRepository
|
|
MockedArtist model.ArtistRepository
|
|
MockedMediaFile model.MediaFileRepository
|
|
MockedTag model.TagRepository
|
|
MockedUser model.UserRepository
|
|
MockedProperty model.PropertyRepository
|
|
MockedPlayer model.PlayerRepository
|
|
MockedPlaylist model.PlaylistRepository
|
|
MockedPlayQueue model.PlayQueueRepository
|
|
MockedShare model.ShareRepository
|
|
MockedTranscoding model.TranscodingRepository
|
|
MockedUserProps model.UserPropsRepository
|
|
MockedScrobbleBuffer model.ScrobbleBufferRepository
|
|
MockedScrobble model.ScrobbleRepository
|
|
MockedRadio model.RadioRepository
|
|
MockedPlugin model.PluginRepository
|
|
MockedArtwork model.ArtworkRepository
|
|
MockedArtworkQueue model.ArtworkQueueRepository
|
|
scrobbleBufferMu sync.Mutex
|
|
repoMu sync.Mutex
|
|
|
|
// GC tracking
|
|
GCCalled bool
|
|
GCError error
|
|
}
|
|
|
|
func (db *MockDataStore) Library(ctx context.Context) model.LibraryRepository {
|
|
db.repoMu.Lock()
|
|
defer db.repoMu.Unlock()
|
|
if db.MockedLibrary != nil {
|
|
return db.MockedLibrary
|
|
}
|
|
if db.RealDS != nil {
|
|
return db.RealDS.Library(ctx)
|
|
}
|
|
db.MockedLibrary = &MockLibraryRepo{}
|
|
return db.MockedLibrary
|
|
}
|
|
|
|
func (db *MockDataStore) Folder(ctx context.Context) model.FolderRepository {
|
|
db.repoMu.Lock()
|
|
defer db.repoMu.Unlock()
|
|
if db.MockedFolder != nil {
|
|
return db.MockedFolder
|
|
}
|
|
if db.RealDS != nil {
|
|
return db.RealDS.Folder(ctx)
|
|
}
|
|
db.MockedFolder = struct{ model.FolderRepository }{}
|
|
return db.MockedFolder
|
|
}
|
|
|
|
func (db *MockDataStore) Tag(ctx context.Context) model.TagRepository {
|
|
db.repoMu.Lock()
|
|
defer db.repoMu.Unlock()
|
|
if db.MockedTag != nil {
|
|
return db.MockedTag
|
|
}
|
|
if db.RealDS != nil {
|
|
return db.RealDS.Tag(ctx)
|
|
}
|
|
db.MockedTag = &MockTagRepo{}
|
|
return db.MockedTag
|
|
}
|
|
|
|
func (db *MockDataStore) Album(ctx context.Context) model.AlbumRepository {
|
|
db.repoMu.Lock()
|
|
defer db.repoMu.Unlock()
|
|
if db.MockedAlbum != nil {
|
|
return db.MockedAlbum
|
|
}
|
|
if db.RealDS != nil {
|
|
return db.RealDS.Album(ctx)
|
|
}
|
|
db.MockedAlbum = CreateMockAlbumRepo()
|
|
return db.MockedAlbum
|
|
}
|
|
|
|
func (db *MockDataStore) Artist(ctx context.Context) model.ArtistRepository {
|
|
db.repoMu.Lock()
|
|
defer db.repoMu.Unlock()
|
|
if db.MockedArtist != nil {
|
|
return db.MockedArtist
|
|
}
|
|
if db.RealDS != nil {
|
|
return db.RealDS.Artist(ctx)
|
|
}
|
|
db.MockedArtist = CreateMockArtistRepo()
|
|
return db.MockedArtist
|
|
}
|
|
|
|
func (db *MockDataStore) MediaFile(ctx context.Context) model.MediaFileRepository {
|
|
db.repoMu.Lock()
|
|
defer db.repoMu.Unlock()
|
|
if db.RealDS != nil && db.MockedMediaFile == nil {
|
|
return db.RealDS.MediaFile(ctx)
|
|
}
|
|
if db.MockedMediaFile == nil {
|
|
db.MockedMediaFile = CreateMockMediaFileRepo()
|
|
}
|
|
return db.MockedMediaFile
|
|
}
|
|
|
|
func (db *MockDataStore) Genre(ctx context.Context) model.GenreRepository {
|
|
db.repoMu.Lock()
|
|
defer db.repoMu.Unlock()
|
|
if db.MockedGenre != nil {
|
|
return db.MockedGenre
|
|
}
|
|
if db.RealDS != nil {
|
|
return db.RealDS.Genre(ctx)
|
|
}
|
|
db.MockedGenre = &MockedGenreRepo{}
|
|
return db.MockedGenre
|
|
}
|
|
|
|
func (db *MockDataStore) Playlist(ctx context.Context) model.PlaylistRepository {
|
|
db.repoMu.Lock()
|
|
defer db.repoMu.Unlock()
|
|
if db.MockedPlaylist != nil {
|
|
return db.MockedPlaylist
|
|
}
|
|
if db.RealDS != nil {
|
|
return db.RealDS.Playlist(ctx)
|
|
}
|
|
db.MockedPlaylist = CreateMockPlaylistRepo()
|
|
return db.MockedPlaylist
|
|
}
|
|
|
|
func (db *MockDataStore) PlayQueue(ctx context.Context) model.PlayQueueRepository {
|
|
db.repoMu.Lock()
|
|
defer db.repoMu.Unlock()
|
|
if db.MockedPlayQueue != nil {
|
|
return db.MockedPlayQueue
|
|
}
|
|
if db.RealDS != nil {
|
|
return db.RealDS.PlayQueue(ctx)
|
|
}
|
|
db.MockedPlayQueue = &MockPlayQueueRepo{}
|
|
return db.MockedPlayQueue
|
|
}
|
|
|
|
func (db *MockDataStore) UserProps(ctx context.Context) model.UserPropsRepository {
|
|
db.repoMu.Lock()
|
|
defer db.repoMu.Unlock()
|
|
if db.MockedUserProps != nil {
|
|
return db.MockedUserProps
|
|
}
|
|
if db.RealDS != nil {
|
|
return db.RealDS.UserProps(ctx)
|
|
}
|
|
db.MockedUserProps = &MockedUserPropsRepo{}
|
|
return db.MockedUserProps
|
|
}
|
|
|
|
func (db *MockDataStore) Property(ctx context.Context) model.PropertyRepository {
|
|
db.repoMu.Lock()
|
|
defer db.repoMu.Unlock()
|
|
if db.MockedProperty != nil {
|
|
return db.MockedProperty
|
|
}
|
|
if db.RealDS != nil {
|
|
return db.RealDS.Property(ctx)
|
|
}
|
|
db.MockedProperty = &MockedPropertyRepo{}
|
|
return db.MockedProperty
|
|
}
|
|
|
|
func (db *MockDataStore) Share(ctx context.Context) model.ShareRepository {
|
|
db.repoMu.Lock()
|
|
defer db.repoMu.Unlock()
|
|
if db.MockedShare != nil {
|
|
return db.MockedShare
|
|
}
|
|
if db.RealDS != nil {
|
|
return db.RealDS.Share(ctx)
|
|
}
|
|
db.MockedShare = &MockShareRepo{}
|
|
return db.MockedShare
|
|
}
|
|
|
|
func (db *MockDataStore) User(ctx context.Context) model.UserRepository {
|
|
db.repoMu.Lock()
|
|
defer db.repoMu.Unlock()
|
|
if db.MockedUser != nil {
|
|
return db.MockedUser
|
|
}
|
|
if db.RealDS != nil {
|
|
return db.RealDS.User(ctx)
|
|
}
|
|
db.MockedUser = CreateMockUserRepo()
|
|
return db.MockedUser
|
|
}
|
|
|
|
func (db *MockDataStore) Transcoding(ctx context.Context) model.TranscodingRepository {
|
|
db.repoMu.Lock()
|
|
defer db.repoMu.Unlock()
|
|
if db.MockedTranscoding != nil {
|
|
return db.MockedTranscoding
|
|
}
|
|
if db.RealDS != nil {
|
|
return db.RealDS.Transcoding(ctx)
|
|
}
|
|
db.MockedTranscoding = struct{ model.TranscodingRepository }{}
|
|
return db.MockedTranscoding
|
|
}
|
|
|
|
func (db *MockDataStore) Player(ctx context.Context) model.PlayerRepository {
|
|
db.repoMu.Lock()
|
|
defer db.repoMu.Unlock()
|
|
if db.MockedPlayer != nil {
|
|
return db.MockedPlayer
|
|
}
|
|
if db.RealDS != nil {
|
|
return db.RealDS.Player(ctx)
|
|
}
|
|
db.MockedPlayer = struct{ model.PlayerRepository }{}
|
|
return db.MockedPlayer
|
|
}
|
|
|
|
func (db *MockDataStore) ScrobbleBuffer(ctx context.Context) model.ScrobbleBufferRepository {
|
|
db.repoMu.Lock()
|
|
defer db.repoMu.Unlock()
|
|
if db.RealDS != nil && db.MockedScrobbleBuffer == nil {
|
|
return db.RealDS.ScrobbleBuffer(ctx)
|
|
}
|
|
db.scrobbleBufferMu.Lock()
|
|
defer db.scrobbleBufferMu.Unlock()
|
|
if db.MockedScrobbleBuffer == nil {
|
|
db.MockedScrobbleBuffer = &MockedScrobbleBufferRepo{}
|
|
}
|
|
return db.MockedScrobbleBuffer
|
|
}
|
|
|
|
func (db *MockDataStore) Scrobble(ctx context.Context) model.ScrobbleRepository {
|
|
db.repoMu.Lock()
|
|
defer db.repoMu.Unlock()
|
|
if db.MockedScrobble != nil {
|
|
return db.MockedScrobble
|
|
}
|
|
if db.RealDS != nil {
|
|
return db.RealDS.Scrobble(ctx)
|
|
}
|
|
db.MockedScrobble = &MockScrobbleRepo{ctx: ctx}
|
|
return db.MockedScrobble
|
|
}
|
|
|
|
func (db *MockDataStore) Radio(ctx context.Context) model.RadioRepository {
|
|
db.repoMu.Lock()
|
|
defer db.repoMu.Unlock()
|
|
if db.MockedRadio != nil {
|
|
return db.MockedRadio
|
|
}
|
|
if db.RealDS != nil {
|
|
return db.RealDS.Radio(ctx)
|
|
}
|
|
db.MockedRadio = CreateMockedRadioRepo()
|
|
return db.MockedRadio
|
|
}
|
|
|
|
func (db *MockDataStore) Plugin(ctx context.Context) model.PluginRepository {
|
|
db.repoMu.Lock()
|
|
defer db.repoMu.Unlock()
|
|
if db.MockedPlugin != nil {
|
|
return db.MockedPlugin
|
|
}
|
|
if db.RealDS != nil {
|
|
return db.RealDS.Plugin(ctx)
|
|
}
|
|
db.MockedPlugin = CreateMockPluginRepo()
|
|
return db.MockedPlugin
|
|
}
|
|
|
|
func (db *MockDataStore) Artwork(ctx context.Context) model.ArtworkRepository {
|
|
db.repoMu.Lock()
|
|
defer db.repoMu.Unlock()
|
|
return db.artworkLocked(ctx)
|
|
}
|
|
|
|
// artworkLocked is the body of Artwork for callers already holding repoMu; repoMu is a plain
|
|
// Mutex, so re-entering through the exported method would deadlock.
|
|
func (db *MockDataStore) artworkLocked(ctx context.Context) model.ArtworkRepository {
|
|
if db.MockedArtwork != nil {
|
|
return db.MockedArtwork
|
|
}
|
|
if db.RealDS != nil {
|
|
return db.RealDS.Artwork(ctx)
|
|
}
|
|
db.MockedArtwork = CreateMockArtworkRepo()
|
|
return db.MockedArtwork
|
|
}
|
|
|
|
func (db *MockDataStore) ArtworkQueue(ctx context.Context) model.ArtworkQueueRepository {
|
|
db.repoMu.Lock()
|
|
defer db.repoMu.Unlock()
|
|
if db.MockedArtworkQueue != nil {
|
|
return db.MockedArtworkQueue
|
|
}
|
|
if db.RealDS != nil {
|
|
return db.RealDS.ArtworkQueue(ctx)
|
|
}
|
|
q := CreateMockArtworkQueueRepo()
|
|
if aw, ok := db.artworkLocked(ctx).(*MockArtworkRepo); ok {
|
|
q.ItemArtworkSource = aw
|
|
}
|
|
db.MockedArtworkQueue = q
|
|
return db.MockedArtworkQueue
|
|
}
|
|
|
|
func (db *MockDataStore) WithTx(block func(tx model.DataStore) error, label ...string) error {
|
|
return block(db)
|
|
}
|
|
|
|
func (db *MockDataStore) WithTxImmediate(block func(tx model.DataStore) error, label ...string) error {
|
|
return block(db)
|
|
}
|
|
|
|
func (db *MockDataStore) Resource(ctx context.Context, m any) model.ResourceRepository {
|
|
switch m.(type) {
|
|
case model.MediaFile, *model.MediaFile:
|
|
return db.MediaFile(ctx).(model.ResourceRepository)
|
|
case model.Album, *model.Album:
|
|
return db.Album(ctx).(model.ResourceRepository)
|
|
case model.Artist, *model.Artist:
|
|
return db.Artist(ctx).(model.ResourceRepository)
|
|
case model.User, *model.User:
|
|
return db.User(ctx).(model.ResourceRepository)
|
|
case model.Playlist, *model.Playlist:
|
|
return db.Playlist(ctx).(model.ResourceRepository)
|
|
case model.Radio, *model.Radio:
|
|
return db.Radio(ctx).(model.ResourceRepository)
|
|
case model.Share, *model.Share:
|
|
return db.Share(ctx).(model.ResourceRepository)
|
|
case model.Genre, *model.Genre:
|
|
return db.Genre(ctx).(model.ResourceRepository)
|
|
case model.Tag, *model.Tag:
|
|
return db.Tag(ctx).(model.ResourceRepository)
|
|
case model.Transcoding, *model.Transcoding:
|
|
return db.Transcoding(ctx).(model.ResourceRepository)
|
|
case model.Player, *model.Player:
|
|
return db.Player(ctx).(model.ResourceRepository)
|
|
case model.Plugin, *model.Plugin:
|
|
return db.Plugin(ctx).(model.ResourceRepository)
|
|
default:
|
|
return struct{ model.ResourceRepository }{}
|
|
}
|
|
}
|
|
|
|
func (db *MockDataStore) GC(context.Context, ...int) error {
|
|
db.GCCalled = true
|
|
if db.GCError != nil {
|
|
return db.GCError
|
|
}
|
|
return nil
|
|
}
|