github.com/derat/nup@v0.0.0-20230418113745-15592ba7c620/cmd/nup/update/scan_test.go (about)

     1  // Copyright 2020 Daniel Erat.
     2  // All rights reserved.
     3  
     4  package update
     5  
     6  import (
     7  	"fmt"
     8  	"io/ioutil"
     9  	"os"
    10  	"path/filepath"
    11  	"reflect"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/derat/nup/cmd/nup/client"
    16  	"github.com/derat/nup/cmd/nup/client/files"
    17  	"github.com/derat/nup/server/db"
    18  	"github.com/derat/nup/test"
    19  )
    20  
    21  // getSongsFromChannel reads and returns num songs from ch.
    22  // If an error was sent to the channel, it is returned.
    23  func getSongsFromChannel(ch chan songOrErr, num int) ([]db.Song, error) {
    24  	var songs []db.Song
    25  	for i := 0; i < num; i++ {
    26  		s := <-ch
    27  		if s.err != nil {
    28  			return nil, fmt.Errorf("got error at position %v instead of song: %v", i, s.err)
    29  		}
    30  		songs = append(songs, *s.song)
    31  	}
    32  	return songs, nil
    33  }
    34  
    35  type scanTestOptions struct {
    36  	metadataDir     string            // client.Config.MetadataDir
    37  	artistRewrites  map[string]string // client.Config.ArtistRewrites
    38  	albumIDRewrites map[string]string // client.Config.AlbumIDRewrites
    39  	lastUpdateDirs  []string          // scanForUpdatedSongs lastUpdateDirs param
    40  	forceGlob       string            // scanOptions.forceGlob
    41  }
    42  
    43  func scanAndCompareSongs(t *testing.T, desc, dir string, lastUpdateTime time.Time,
    44  	testOpts *scanTestOptions, expected []db.Song) (dirs []string) {
    45  	cfg := &client.Config{MusicDir: dir}
    46  	opts := &scanOptions{}
    47  	var lastUpdateDirs []string
    48  	if testOpts != nil {
    49  		cfg.MetadataDir = testOpts.metadataDir
    50  		cfg.ArtistRewrites = testOpts.artistRewrites
    51  		cfg.AlbumIDRewrites = testOpts.albumIDRewrites
    52  		opts.forceGlob = testOpts.forceGlob
    53  		lastUpdateDirs = testOpts.lastUpdateDirs
    54  	}
    55  	ch := make(chan songOrErr)
    56  	num, dirs, err := scanForUpdatedSongs(cfg, lastUpdateTime, lastUpdateDirs, ch, opts)
    57  	if err != nil {
    58  		t.Errorf("%v: %v", desc, err)
    59  		return dirs
    60  	}
    61  	actual, err := getSongsFromChannel(ch, num)
    62  	if err != nil {
    63  		t.Errorf("%v: %v", desc, err)
    64  		return dirs
    65  	}
    66  	for i := range expected {
    67  		found := false
    68  		for j := range actual {
    69  			if expected[i].Filename == actual[j].Filename {
    70  				found = true
    71  				if expected[i].RecordingID != actual[j].RecordingID {
    72  					t.Errorf("%v: song %v didn't have expected recording id: expected %q, actual %q",
    73  						desc, i, expected[i].RecordingID, actual[j].RecordingID)
    74  					return dirs
    75  				}
    76  				expected[i].Rating = 0
    77  				expected[i].TrackGain = 0
    78  				expected[i].AlbumGain = 0
    79  				expected[i].PeakAmp = 0
    80  				break
    81  			}
    82  		}
    83  		if !found {
    84  			t.Errorf("%v: didn't get song %v", desc, i)
    85  		}
    86  	}
    87  	if err := test.CompareSongs(expected, actual, test.IgnoreOrder); err != nil {
    88  		t.Errorf("%v: %v", desc, err)
    89  	}
    90  	return dirs
    91  }
    92  
    93  func TestScanAndCompareSongs(t *testing.T) {
    94  	dir := t.TempDir()
    95  	test.Must(t, test.CopySongs(dir, test.Song0s.Filename, test.Song1s.Filename))
    96  	startTime := time.Now()
    97  	scanAndCompareSongs(t, "initial", dir, time.Time{}, nil, []db.Song{test.Song0s, test.Song1s})
    98  	scanAndCompareSongs(t, "unchanged", dir, startTime, nil, []db.Song{})
    99  
   100  	test.Must(t, test.CopySongs(dir, test.Song5s.Filename))
   101  	addTime := time.Now()
   102  	scanAndCompareSongs(t, "add", dir, startTime, nil, []db.Song{test.Song5s})
   103  
   104  	if err := os.Remove(filepath.Join(dir, test.Song0s.Filename)); err != nil {
   105  		t.Fatal("Failed removing song: ", err)
   106  	}
   107  	test.Must(t, test.CopySongs(dir, test.Song0sUpdated.Filename))
   108  	updateTime := time.Now()
   109  	scanAndCompareSongs(t, "update", dir, addTime, nil, []db.Song{test.Song0sUpdated})
   110  
   111  	subdir := filepath.Join(dir, "foo")
   112  	if err := os.Mkdir(subdir, 0700); err != nil {
   113  		t.Fatal("Failed making subdir: ", err)
   114  	}
   115  	renamedPath := filepath.Join(subdir, test.Song1s.Filename)
   116  	if err := os.Rename(filepath.Join(dir, test.Song1s.Filename), renamedPath); err != nil {
   117  		t.Fatal("Failed renaming song: ", err)
   118  	}
   119  	now := time.Now()
   120  	if err := os.Chtimes(renamedPath, now, now); err != nil {
   121  		t.Fatal("Failed setting times: ", err)
   122  	}
   123  	renamedSong1s := test.Song1s
   124  	renamedSong1s.Filename = filepath.Join(filepath.Base(subdir), test.Song1s.Filename)
   125  	scanAndCompareSongs(t, "rename", dir, updateTime, nil, []db.Song{renamedSong1s})
   126  
   127  	scanAndCompareSongs(t, "force exact", dir, updateTime,
   128  		&scanTestOptions{forceGlob: test.Song0sUpdated.Filename},
   129  		[]db.Song{test.Song0sUpdated})
   130  	scanAndCompareSongs(t, "force wildcard", dir, updateTime,
   131  		&scanTestOptions{forceGlob: "foo/*"},
   132  		[]db.Song{renamedSong1s})
   133  
   134  	updateTime = time.Now()
   135  	test.Must(t, test.CopySongs(dir, test.ID3V1Song.Filename))
   136  	scanAndCompareSongs(t, "id3v1", dir, updateTime, nil, []db.Song{test.ID3V1Song})
   137  }
   138  
   139  func TestScanAndCompareSongs_Rewrite(t *testing.T) {
   140  	dir := t.TempDir()
   141  
   142  	newSong1s := test.Song1s
   143  	newSong1s.Artist = "Rewritten Artist"
   144  
   145  	// The album name, disc number, and disc subtitle should all be derived from
   146  	// the original "Another Album (disc 3: The Third Disc)" album name.
   147  	newSong5s := test.Song5s
   148  	newSong5s.Album = "Another Album"
   149  	newSong5s.AlbumID = "bf7ff94c-2a6a-4357-a30e-71da8c117ebc"
   150  	newSong5s.CoverID = test.Song5s.AlbumID
   151  	newSong5s.Disc = 3
   152  	newSong5s.DiscSubtitle = "The Third Disc"
   153  
   154  	// This also verifies that Song10s's TSST frame is used to fill DiscSubtitle.
   155  	test.Must(t, test.CopySongs(dir, test.Song1s.Filename, test.Song5s.Filename, test.Song10s.Filename))
   156  	opts := &scanTestOptions{
   157  		artistRewrites:  map[string]string{test.Song1s.Artist: newSong1s.Artist},
   158  		albumIDRewrites: map[string]string{test.Song5s.AlbumID: newSong5s.AlbumID},
   159  	}
   160  	scanAndCompareSongs(t, "initial", dir, time.Time{}, opts, []db.Song{newSong1s, newSong5s, test.Song10s})
   161  }
   162  
   163  func TestScanAndCompareSongs_NewFiles(t *testing.T) {
   164  	dir := t.TempDir()
   165  	const (
   166  		oldArtist = "old_artist"
   167  		oldAlbum  = "old_album"
   168  		newAlbum  = "new_album"
   169  		newArtist = "new_artist"
   170  	)
   171  
   172  	// Start out with an artist/album directory containing a single song.
   173  	musicDir := filepath.Join(dir, "music")
   174  	test.Must(t, test.CopySongs(filepath.Join(musicDir, oldArtist, oldAlbum), test.Song0s.Filename))
   175  
   176  	// Copy some more songs into the temp dir to give them old timestamps,
   177  	// but don't move them under the music dir yet.
   178  	test.Must(t, test.CopySongs(dir, test.Song1s.Filename))
   179  	test.Must(t, test.CopySongs(filepath.Join(dir, newAlbum), test.Song5s.Filename))
   180  	test.Must(t, test.CopySongs(filepath.Join(dir, newArtist, newAlbum), test.ID3V1Song.Filename))
   181  
   182  	// Updates the supplied song's filename to be under dir.
   183  	gs := func(s db.Song, dir string) db.Song {
   184  		s.Filename = filepath.Join(dir, s.Filename)
   185  		return s
   186  	}
   187  
   188  	startTime := time.Now()
   189  	origDirs := scanAndCompareSongs(t, "initial", musicDir, time.Time{}, nil,
   190  		[]db.Song{gs(test.Song0s, filepath.Join(oldArtist, oldAlbum))})
   191  	if want := []string{filepath.Join(oldArtist, oldAlbum)}; !reflect.DeepEqual(origDirs, want) {
   192  		t.Errorf("scanAndCompareSongs(...) = %v; want %v", origDirs, want)
   193  	}
   194  
   195  	// Move the new files into various locations under the music dir.
   196  	mv := func(src, dst string) {
   197  		if err := os.Rename(src, dst); err != nil {
   198  			t.Fatal("Failed renaming file: ", err)
   199  		}
   200  	}
   201  	waitCtime(t, dir, startTime)
   202  	mv(filepath.Join(dir, test.Song1s.Filename),
   203  		filepath.Join(musicDir, oldArtist, oldAlbum, test.Song1s.Filename))
   204  	mv(filepath.Join(dir, newAlbum),
   205  		filepath.Join(musicDir, oldArtist, newAlbum))
   206  	mv(filepath.Join(dir, newArtist), filepath.Join(musicDir, newArtist))
   207  
   208  	// All three of the new songs should be seen.
   209  	updateTime := time.Now()
   210  	newDirs := scanAndCompareSongs(t, "updated", musicDir, startTime,
   211  		&scanTestOptions{lastUpdateDirs: origDirs},
   212  		[]db.Song{
   213  			gs(test.Song1s, filepath.Join(oldArtist, oldAlbum)),
   214  			gs(test.Song5s, filepath.Join(oldArtist, newAlbum)),
   215  			gs(test.ID3V1Song, filepath.Join(newArtist, newAlbum)),
   216  		})
   217  	allDirs := []string{
   218  		filepath.Join(newArtist, newAlbum),
   219  		filepath.Join(oldArtist, newAlbum),
   220  		filepath.Join(oldArtist, oldAlbum),
   221  	}
   222  	if !reflect.DeepEqual(newDirs, allDirs) {
   223  		t.Errorf("scanAndCompareSongs(...) = %v; want %v", newDirs, allDirs)
   224  	}
   225  
   226  	// Do one more scan and check that no songs are returned.
   227  	newDirs = scanAndCompareSongs(t, "rescan", musicDir, updateTime,
   228  		&scanTestOptions{lastUpdateDirs: newDirs}, []db.Song{})
   229  	if !reflect.DeepEqual(newDirs, allDirs) {
   230  		t.Errorf("scanAndCompareSongs(...) = %v; want %v", newDirs, allDirs)
   231  	}
   232  }
   233  
   234  func TestScanAndCompareSongs_OverrideMetadata(t *testing.T) {
   235  	td := t.TempDir()
   236  	musicDir := filepath.Join(td, "music")
   237  	test.Must(t, test.CopySongs(musicDir, test.Song1s.Filename))
   238  
   239  	metadataDir := filepath.Join(td, "metadata")
   240  	cfg := &client.Config{MusicDir: musicDir, MetadataDir: metadataDir}
   241  	opts := &scanTestOptions{metadataDir: metadataDir}
   242  
   243  	// Perform an initial scan to pick up the song.
   244  	startTime := time.Now()
   245  	scanAndCompareSongs(t, "initial", musicDir, time.Time{}, opts, []db.Song{test.Song1s})
   246  
   247  	// Write a file to override the song's metadata.
   248  	updated := test.Song1s
   249  	updated.Artist = "New Artist"
   250  	updated.Title = "New Title"
   251  	waitCtime(t, metadataDir, startTime)
   252  	test.Must(t, files.UpdateMetadataOverride(cfg, &updated))
   253  
   254  	// The next scan should pick up the new metadata even though the song file wasn't updated.
   255  	overrideTime := time.Now()
   256  	scanAndCompareSongs(t, "wrote override", musicDir, startTime, opts, []db.Song{updated})
   257  
   258  	// Clear the override file and scan again to go back to the original metadata.
   259  	waitCtime(t, metadataDir, overrideTime)
   260  	test.Must(t, files.UpdateMetadataOverride(cfg, &test.Song1s))
   261  	clearTime := time.Now()
   262  	scanAndCompareSongs(t, "cleared override", musicDir, overrideTime, opts, []db.Song{test.Song1s})
   263  
   264  	// Nothing should happen after doing a scan without any changes.
   265  	scanAndCompareSongs(t, "no change", musicDir, clearTime, opts, []db.Song{})
   266  }
   267  
   268  // TODO: Test errors, skipping bogus files, etc.
   269  
   270  // waitCtime waits until the kernel is assigning ctimes after ref to new files in dir.
   271  // This is super-cheesy, but ctimes (and mtimes?) appear to get rounded, so it's sometimes
   272  // (often) the case that a newly-created file's ctime/mtime will precede the time returned
   273  // by an earlier time.Now() call.
   274  func waitCtime(t *testing.T, dir string, ref time.Time) {
   275  	if err := os.MkdirAll(dir, 0755); err != nil {
   276  		t.Fatal(err)
   277  	}
   278  	for {
   279  		if func() time.Time {
   280  			f, err := ioutil.TempFile(dir, "temp.")
   281  			if err != nil {
   282  				t.Fatal(err)
   283  			}
   284  			defer os.Remove(f.Name())
   285  			defer f.Close()
   286  			fi, err := f.Stat()
   287  			if err != nil {
   288  				t.Fatal(err)
   289  			}
   290  			return getCtime(fi)
   291  		}().After(ref) {
   292  			break
   293  		}
   294  		time.Sleep(time.Millisecond)
   295  	}
   296  }