1 /*
2  * Strawberry Music Player
3  * This file was part of Clementine.
4  * Copyright 2010, David Sansome <me@davidsansome.com>
5  * Copyright 2019, Jonas Kvinge <jonas@jkvinge.net>
6  *
7  * Strawberry is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11  *
12  * Strawberry is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with Strawberry.  If not, see <http://www.gnu.org/licenses/>.
19  *
20  */
21 
22 #include <memory>
23 
24 #include <gtest/gtest.h>
25 
26 #include <QFileInfo>
27 #include <QSignalSpy>
28 #include <QThread>
29 #include <QtDebug>
30 
31 #include "test_utils.h"
32 
33 #include "core/timeconstants.h"
34 #include "core/song.h"
35 #include "core/database.h"
36 #include "core/logging.h"
37 #include "collection/collectionbackend.h"
38 #include "collection/collection.h"
39 
40 // clazy:excludeall=non-pod-global-static,returning-void-expression
41 
42 namespace {
43 
44 class CollectionBackendTest : public ::testing::Test {
45  protected:
SetUp()46   void SetUp() override {
47     database_.reset(new MemoryDatabase(nullptr));
48     backend_ = std::make_unique<CollectionBackend>();
49     backend_->Init(database_.get(), nullptr, Song::Source_Collection, SCollection::kSongsTable, SCollection::kFtsTable, SCollection::kDirsTable, SCollection::kSubdirsTable);
50   }
51 
MakeDummySong(int directory_id)52   static Song MakeDummySong(int directory_id) {
53     // Returns a valid song with all the required fields set
54     Song ret;
55     ret.set_directory_id(directory_id);
56     ret.set_url(QUrl::fromLocalFile("foo.flac"));
57     ret.set_mtime(1);
58     ret.set_ctime(1);
59     ret.set_filesize(1);
60     return ret;
61   }
62 
63   std::shared_ptr<Database> database_;  // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes)
64   std::unique_ptr<CollectionBackend> backend_;  // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes)
65 };
66 
TEST_F(CollectionBackendTest,EmptyDatabase)67 TEST_F(CollectionBackendTest, EmptyDatabase) {
68 
69   // Check the database is empty to start with
70   QStringList artists = backend_->GetAllArtists();
71   EXPECT_TRUE(artists.isEmpty());
72 
73   CollectionBackend::AlbumList albums = backend_->GetAllAlbums();
74   EXPECT_TRUE(albums.isEmpty());
75 
76 }
77 
TEST_F(CollectionBackendTest,AddDirectory)78 TEST_F(CollectionBackendTest, AddDirectory) {
79 
80   QSignalSpy spy(backend_.get(), &CollectionBackend::DirectoryDiscovered);
81 
82   backend_->AddDirectory("/tmp");
83 
84   // Check the signal was emitted correctly
85   ASSERT_EQ(1, spy.count());
86   Directory dir = spy[0][0].value<Directory>();
87   EXPECT_EQ(QFileInfo("/tmp").canonicalFilePath(), dir.path);
88   EXPECT_EQ(1, dir.id);
89   EXPECT_EQ(0, spy[0][1].value<SubdirectoryList>().size());
90 
91 }
92 
TEST_F(CollectionBackendTest,RemoveDirectory)93 TEST_F(CollectionBackendTest, RemoveDirectory) {
94 
95   // Add a directory
96   Directory dir;
97   dir.id = 1;
98   dir.path = "/tmp";
99   backend_->AddDirectory(dir.path);
100 
101   QSignalSpy spy(backend_.get(), &CollectionBackend::DirectoryDeleted);
102 
103   // Remove the directory again
104   backend_->RemoveDirectory(dir);
105 
106   // Check the signal was emitted correctly
107   ASSERT_EQ(1, spy.count());
108   dir = spy[0][0].value<Directory>();
109   EXPECT_EQ("/tmp", dir.path);
110   EXPECT_EQ(1, dir.id);
111 
112 }
113 
TEST_F(CollectionBackendTest,AddInvalidSong)114 TEST_F(CollectionBackendTest, AddInvalidSong) {
115 
116   // Adding a song without certain fields set should fail
117   backend_->AddDirectory("/tmp");
118   Song s;
119   s.set_url(QUrl::fromLocalFile("foo.flac"));
120   s.set_directory_id(1);
121 
122   QSignalSpy spy(database_.get(), &Database::Error);
123 
124   backend_->AddOrUpdateSongs(SongList() << s);
125   ASSERT_EQ(1, spy.count());
126   spy.takeFirst();
127 
128   s.set_url(QUrl::fromLocalFile("foo.flac"));
129   backend_->AddOrUpdateSongs(SongList() << s);
130   ASSERT_EQ(1, spy.count());
131   spy.takeFirst();
132 
133   s.set_filesize(100);
134   backend_->AddOrUpdateSongs(SongList() << s);
135   ASSERT_EQ(1, spy.count());
136   spy.takeFirst();
137 
138   s.set_mtime(100);
139   backend_->AddOrUpdateSongs(SongList() << s);
140   ASSERT_EQ(1, spy.count());
141   spy.takeFirst();
142 
143   s.set_ctime(100);
144   backend_->AddOrUpdateSongs(SongList() << s);
145   ASSERT_EQ(0, spy.count());
146 
147 }
148 
TEST_F(CollectionBackendTest,GetAlbumArtNonExistent)149 TEST_F(CollectionBackendTest, GetAlbumArtNonExistent) {}
150 
151 // Test adding a single song to the database, then getting various information back about it.
152 class SingleSong : public CollectionBackendTest {
153  protected:
SetUp()154   void SetUp() override {
155     CollectionBackendTest::SetUp();
156 
157     // Add a directory - this will get ID 1
158     backend_->AddDirectory("/tmp");
159 
160     // Make a song in that directory
161     song_ = MakeDummySong(1);
162     song_.set_title("Title");
163     song_.set_artist("Artist");
164     song_.set_album("Album");
165     song_.set_url(QUrl::fromLocalFile("foo.flac"));
166   }
167 
AddDummySong()168   void AddDummySong() {
169     QSignalSpy added_spy(backend_.get(), &CollectionBackend::SongsDiscovered);
170     QSignalSpy deleted_spy(backend_.get(), &CollectionBackend::SongsDeleted);
171 
172     // Add the song
173     backend_->AddOrUpdateSongs(SongList() << song_);
174 
175     // Check the correct signals were emitted
176     EXPECT_EQ(0, deleted_spy.count());
177     ASSERT_EQ(1, added_spy.count());
178 
179     SongList list = *(reinterpret_cast<SongList*>(added_spy[0][0].data()));
180     ASSERT_EQ(1, list.count());
181     EXPECT_EQ(song_.title(), list[0].title());
182     EXPECT_EQ(song_.artist(), list[0].artist());
183     EXPECT_EQ(song_.album(), list[0].album());
184     EXPECT_EQ(1, list[0].id());
185     EXPECT_EQ(1, list[0].directory_id());
186   }
187 
188   Song song_;  // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes)
189 
190 };
191 
TEST_F(SingleSong,GetSongWithNoAlbum)192 TEST_F(SingleSong, GetSongWithNoAlbum) {
193 
194   song_.set_album("");
195   AddDummySong();
196   if (HasFatalFailure()) return;
197 
198   EXPECT_EQ(1, backend_->GetAllArtists().size());
199   CollectionBackend::AlbumList albums = backend_->GetAllAlbums();
200   EXPECT_EQ(1, albums.size());
201   EXPECT_EQ("Artist", albums[0].album_artist);
202   EXPECT_EQ("", albums[0].album);
203 
204 }
205 
TEST_F(SingleSong,GetAllArtists)206 TEST_F(SingleSong, GetAllArtists) {
207 
208   AddDummySong();
209   if (HasFatalFailure()) return;
210 
211   QStringList artists = backend_->GetAllArtists();
212   ASSERT_EQ(1, artists.size());
213   EXPECT_EQ(song_.artist(), artists[0]);
214 
215 }
216 
TEST_F(SingleSong,GetAllAlbums)217 TEST_F(SingleSong, GetAllAlbums) {
218 
219   AddDummySong();
220   if (HasFatalFailure()) return;
221 
222   CollectionBackend::AlbumList albums = backend_->GetAllAlbums();
223   ASSERT_EQ(1, albums.size());
224   EXPECT_EQ(song_.album(), albums[0].album);
225   EXPECT_EQ(song_.artist(), albums[0].album_artist);
226 
227 }
228 
TEST_F(SingleSong,GetAlbumsByArtist)229 TEST_F(SingleSong, GetAlbumsByArtist) {
230 
231   AddDummySong();
232   if (HasFatalFailure()) return;
233 
234   CollectionBackend::AlbumList albums = backend_->GetAlbumsByArtist("Artist");
235   ASSERT_EQ(1, albums.size());
236   EXPECT_EQ(song_.album(), albums[0].album);
237   EXPECT_EQ(song_.artist(), albums[0].album_artist);
238 
239 }
240 
TEST_F(SingleSong,GetAlbumArt)241 TEST_F(SingleSong, GetAlbumArt) {
242 
243   AddDummySong();
244   if (HasFatalFailure()) return;
245 
246   CollectionBackend::Album album = backend_->GetAlbumArt("Artist", "Album");
247   EXPECT_EQ(song_.album(), album.album);
248   EXPECT_EQ(song_.effective_albumartist(), album.album_artist);
249 
250 }
251 
TEST_F(SingleSong,GetSongs)252 TEST_F(SingleSong, GetSongs) {
253 
254   AddDummySong();
255   if (HasFatalFailure()) return;
256 
257   SongList songs = backend_->GetAlbumSongs("Artist", "Album");
258   ASSERT_EQ(1, songs.size());
259   EXPECT_EQ(song_.album(), songs[0].album());
260   EXPECT_EQ(song_.artist(), songs[0].artist());
261   EXPECT_EQ(song_.title(), songs[0].title());
262   EXPECT_EQ(1, songs[0].id());
263 
264 }
265 
TEST_F(SingleSong,GetSongById)266 TEST_F(SingleSong, GetSongById) {
267 
268   AddDummySong();
269   if (HasFatalFailure()) return;
270 
271   Song song = backend_->GetSongById(1);
272   EXPECT_EQ(song_.album(), song.album());
273   EXPECT_EQ(song_.artist(), song.artist());
274   EXPECT_EQ(song_.title(), song.title());
275   EXPECT_EQ(1, song.id());
276 
277 }
278 
TEST_F(SingleSong,FindSongsInDirectory)279 TEST_F(SingleSong, FindSongsInDirectory) {
280 
281   AddDummySong();
282   if (HasFatalFailure()) return;
283 
284   SongList songs = backend_->FindSongsInDirectory(1);
285   ASSERT_EQ(1, songs.size());
286   EXPECT_EQ(song_.album(), songs[0].album());
287   EXPECT_EQ(song_.artist(), songs[0].artist());
288   EXPECT_EQ(song_.title(), songs[0].title());
289   EXPECT_EQ(1, songs[0].id());
290 
291 }
292 
TEST_F(SingleSong,UpdateSong)293 TEST_F(SingleSong, UpdateSong) {
294 
295   AddDummySong();
296   if (HasFatalFailure()) return;
297 
298   Song new_song(song_);
299   new_song.set_id(1);
300   new_song.set_title("A different title");
301 
302   QSignalSpy deleted_spy(backend_.get(), &CollectionBackend::SongsDeleted);
303   QSignalSpy added_spy(backend_.get(), &CollectionBackend::SongsDiscovered);
304 
305   backend_->AddOrUpdateSongs(SongList() << new_song);
306 
307   ASSERT_EQ(1, added_spy.size());
308   ASSERT_EQ(1, deleted_spy.size());
309 
310   SongList songs_added = *(reinterpret_cast<SongList*>(added_spy[0][0].data()));
311   SongList songs_deleted = *(reinterpret_cast<SongList*>(deleted_spy[0][0].data()));
312   ASSERT_EQ(1, songs_added.size());
313   ASSERT_EQ(1, songs_deleted.size());
314   EXPECT_EQ("Title", songs_deleted[0].title());
315   EXPECT_EQ("A different title", songs_added[0].title());
316   EXPECT_EQ(1, songs_deleted[0].id());
317   EXPECT_EQ(1, songs_added[0].id());
318 
319 }
320 
TEST_F(SingleSong,DeleteSongs)321 TEST_F(SingleSong, DeleteSongs) {
322 
323   AddDummySong();
324   if (HasFatalFailure()) return;
325 
326   Song new_song(song_);
327   new_song.set_id(1);
328 
329   QSignalSpy deleted_spy(backend_.get(), &CollectionBackend::SongsDeleted);
330 
331   backend_->DeleteSongs(SongList() << new_song);
332 
333   ASSERT_EQ(1, deleted_spy.size());
334 
335   SongList songs_deleted = *(reinterpret_cast<SongList*>(deleted_spy[0][0].data()));
336   ASSERT_EQ(1, songs_deleted.size());
337   EXPECT_EQ("Title", songs_deleted[0].title());
338   EXPECT_EQ(1, songs_deleted[0].id());
339 
340   // Check we can't retrieve that song any more
341   Song song = backend_->GetSongById(1);
342   EXPECT_FALSE(song.is_valid());
343   EXPECT_EQ(-1, song.id());
344 
345   // And the artist or album shouldn't show up either
346   QStringList artists = backend_->GetAllArtists();
347   EXPECT_EQ(0, artists.size());
348 
349   CollectionBackend::AlbumList albums = backend_->GetAllAlbums();
350   EXPECT_EQ(0, albums.size());
351 
352 }
353 
TEST_F(SingleSong,MarkSongsUnavailable)354 TEST_F(SingleSong, MarkSongsUnavailable) {
355 
356   AddDummySong();
357   if (HasFatalFailure()) return;
358 
359   Song new_song(song_);
360   new_song.set_id(1);
361 
362   QSignalSpy deleted_spy(backend_.get(), &CollectionBackend::SongsDeleted);
363 
364   backend_->MarkSongsUnavailable(SongList() << new_song);
365 
366   ASSERT_EQ(1, deleted_spy.size());
367 
368   SongList songs_deleted = *(reinterpret_cast<SongList*>(deleted_spy[0][0].data()));
369   ASSERT_EQ(1, songs_deleted.size());
370   EXPECT_EQ("Title", songs_deleted[0].title());
371   EXPECT_EQ(1, songs_deleted[0].id());
372 
373   // Check the song is marked as deleted.
374   Song song = backend_->GetSongById(1);
375   EXPECT_TRUE(song.is_valid());
376   EXPECT_TRUE(song.is_unavailable());
377 
378   // And the artist or album shouldn't show up either
379   QStringList artists = backend_->GetAllArtists();
380   EXPECT_EQ(0, artists.size());
381 
382   CollectionBackend::AlbumList albums = backend_->GetAllAlbums();
383   EXPECT_EQ(0, albums.size());
384 
385 }
386 
387 class TestUrls : public CollectionBackendTest {
388  protected:
SetUp()389   void SetUp() override {
390     CollectionBackendTest::SetUp();
391     backend_->AddDirectory("/mnt/music");
392   }
393 };
394 
TEST_F(TestUrls,TestUrls)395 TEST_F(TestUrls, TestUrls) {
396 
397   QStringList strings = QStringList() << "file:///mnt/music/01 - Pink Floyd - Echoes.flac"
398                                       << "file:///mnt/music/02 - Björn Afzelius - Det räcker nu.flac"
399                                       << "file:///mnt/music/03 - Vazelina Bilopphøggers - Bomull i øra.flac"
400                                       << "file:///mnt/music/Test !#$%&'()-@^_`{}~..flac";
401 
402   QList<QUrl> urls = QUrl::fromStringList(strings);
403   SongList songs;
404   for (const QUrl &url : urls) {
405 
406     EXPECT_EQ(url, QUrl::fromEncoded(url.toString(QUrl::FullyEncoded).toUtf8()));
407     EXPECT_EQ(url.toString(QUrl::FullyEncoded), url.toEncoded());
408 
409     Song song(Song::Source_Collection);
410     song.set_directory_id(1);
411     song.set_title("Test Title");
412     song.set_album("Test Album");
413     song.set_artist("Test Artist");
414     song.set_url(url);
415     song.set_length_nanosec(kNsecPerSec);
416     song.set_mtime(1);
417     song.set_ctime(1);
418     song.set_filesize(1);
419     song.set_valid(true);
420 
421     songs << song;
422 
423   }
424 
425   QSignalSpy spy(backend_.get(), &CollectionBackend::SongsDiscovered);
426 
427   backend_->AddOrUpdateSongs(songs);
428   if (HasFatalFailure()) return;
429 
430   ASSERT_EQ(1, spy.count());
431   SongList new_songs = spy[0][0].value<SongList>();
432   EXPECT_EQ(new_songs.count(), strings.count());
433 
434   for (const QUrl &url : urls) {
435 
436     songs = backend_->GetSongsByUrl(url);
437     EXPECT_EQ(1, songs.count());
438     if (songs.count() < 1) continue;
439 
440     Song new_song = songs.first();
441     EXPECT_TRUE(new_song.is_valid());
442     EXPECT_EQ(new_song.url(), url);
443 
444     new_song = backend_->GetSongByUrl(url);
445     EXPECT_EQ(1, songs.count());
446     if (songs.count() < 1) continue;
447 
448     EXPECT_TRUE(new_song.is_valid());
449     EXPECT_EQ(new_song.url(), url);
450 
451     QSqlDatabase db(database_->Connect());
452     QSqlQuery q(db);
453     q.prepare(QString("SELECT url FROM %1 WHERE url = :url").arg(SCollection::kSongsTable));
454 
455     q.bindValue(":url", url.toString(QUrl::FullyEncoded));
456     EXPECT_TRUE(q.exec());
457 
458     while (q.next()) {
459       EXPECT_EQ(url, q.value(0).toUrl());
460       EXPECT_EQ(url, QUrl::fromEncoded(q.value(0).toByteArray()));
461     }
462 
463   }
464 
465 }
466 
467 class UpdateSongsBySongID : public CollectionBackendTest {
468  protected:
SetUp()469   void SetUp() override {
470     CollectionBackendTest::SetUp();
471     backend_->AddDirectory("/mnt/music");
472   }
473 };
474 
TEST_F(UpdateSongsBySongID,UpdateSongsBySongID)475 TEST_F(UpdateSongsBySongID, UpdateSongsBySongID) {
476 
477     QStringList song_ids = QStringList() << "song1"
478                                          << "song2"
479                                          << "song3"
480                                          << "song4"
481                                          << "song5"
482                                          << "song6";
483 
484   { // Add songs
485     SongMap songs;
486 
487     for (const QString &song_id : song_ids) {
488 
489       QUrl url;
490       url.setScheme("file");
491       url.setPath("/music/" + song_id);
492 
493       Song song(Song::Source_Collection);
494       song.set_song_id(song_id);
495       song.set_directory_id(1);
496       song.set_title("Test Title " + song_id);
497       song.set_album("Test Album");
498       song.set_artist("Test Artist");
499       song.set_url(url);
500       song.set_length_nanosec(kNsecPerSec);
501       song.set_mtime(1);
502       song.set_ctime(1);
503       song.set_filesize(1);
504       song.set_valid(true);
505 
506       songs.insert(song_id, song);
507 
508     }
509 
510     QSignalSpy spy(backend_.get(), &CollectionBackend::SongsDiscovered);
511 
512     backend_->UpdateSongsBySongID(songs);
513 
514     ASSERT_EQ(1, spy.count());
515     SongList new_songs = spy[0][0].value<SongList>();
516     EXPECT_EQ(new_songs.count(), song_ids.count());
517     EXPECT_EQ(song_ids[0], new_songs[0].song_id());
518     EXPECT_EQ(song_ids[1], new_songs[1].song_id());
519     EXPECT_EQ(song_ids[2], new_songs[2].song_id());
520     EXPECT_EQ(song_ids[3], new_songs[3].song_id());
521     EXPECT_EQ(song_ids[4], new_songs[4].song_id());
522     EXPECT_EQ(song_ids[5], new_songs[5].song_id());
523 
524   }
525 
526   {  // Check that all songs are added.
527 
528     SongMap songs;
529     {
530       QSqlDatabase db(database_->Connect());
531       CollectionQuery query(db, SCollection::kSongsTable, SCollection::kFtsTable);
532       EXPECT_TRUE(backend_->ExecCollectionQuery(&query, songs));
533     }
534 
535     EXPECT_EQ(songs.count(), song_ids.count());
536 
537     for (QMap<QString, Song>::const_iterator it = songs.begin() ; it != songs.end() ; ++it) {
538       EXPECT_EQ(it.key(), it.value().song_id());
539     }
540 
541     for (const QString &song_id : song_ids) {
542       EXPECT_TRUE(songs.contains(song_id));
543     }
544 
545   }
546 
547   {  // Remove some songs
548     QSignalSpy spy1(backend_.get(), &CollectionBackend::SongsDiscovered);
549     QSignalSpy spy2(backend_.get(), &CollectionBackend::SongsDeleted);
550 
551     SongMap songs;
552 
553     QStringList song_ids2 = QStringList() << "song1"
554                                           << "song4"
555                                           << "song5"
556                                           << "song6";
557 
558     for (const QString &song_id : song_ids2) {
559 
560       QUrl url;
561       url.setScheme("file");
562       url.setPath("/music/" + song_id);
563 
564       Song song(Song::Source_Collection);
565       song.set_song_id(song_id);
566       song.set_directory_id(1);
567       song.set_title("Test Title " + song_id);
568       song.set_album("Test Album");
569       song.set_artist("Test Artist");
570       song.set_url(url);
571       song.set_length_nanosec(kNsecPerSec);
572       song.set_mtime(1);
573       song.set_ctime(1);
574       song.set_filesize(1);
575       song.set_valid(true);
576 
577       songs.insert(song_id, song);
578 
579     }
580 
581     backend_->UpdateSongsBySongID(songs);
582 
583     ASSERT_EQ(0, spy1.count());
584     ASSERT_EQ(1, spy2.count());
585     SongList deleted_songs = spy2[0][0].value<SongList>();
586     EXPECT_EQ(deleted_songs.count(), 2);
587     EXPECT_EQ(deleted_songs[0].song_id(), "song2");
588     EXPECT_EQ(deleted_songs[1].song_id(), "song3");
589 
590   }
591 
592   {  // Update some songs
593     QSignalSpy spy1(backend_.get(), &CollectionBackend::SongsDeleted);
594     QSignalSpy spy2(backend_.get(), &CollectionBackend::SongsDiscovered);
595 
596     SongMap songs;
597 
598     QStringList song_ids2 = QStringList() << "song1"
599                                           << "song4"
600                                           << "song5"
601                                           << "song6";
602 
603     for (const QString &song_id : song_ids2) {
604 
605       QUrl url;
606       url.setScheme("file");
607       url.setPath("/music/" + song_id);
608 
609       Song song(Song::Source_Collection);
610       song.set_song_id(song_id);
611       song.set_directory_id(1);
612       song.set_title("Test Title " + song_id);
613       song.set_album("Test Album");
614       song.set_artist("Test Artist");
615       song.set_url(url);
616       song.set_length_nanosec(kNsecPerSec);
617       song.set_mtime(1);
618       song.set_ctime(1);
619       song.set_filesize(1);
620       song.set_valid(true);
621 
622       songs.insert(song_id, song);
623 
624     }
625 
626     songs["song1"].set_artist("New artist");
627     songs["song6"].set_artist("New artist");
628 
629     backend_->UpdateSongsBySongID(songs);
630 
631     ASSERT_EQ(1, spy1.count());
632     ASSERT_EQ(1, spy2.count());
633     SongList deleted_songs = spy1[0][0].value<SongList>();
634     SongList added_songs = spy2[0][0].value<SongList>();
635     EXPECT_EQ(deleted_songs.count(), 2);
636     EXPECT_EQ(added_songs.count(), 2);
637     EXPECT_EQ(deleted_songs[0].song_id(), "song1");
638     EXPECT_EQ(deleted_songs[1].song_id(), "song6");
639     EXPECT_EQ(added_songs[0].song_id(), "song1");
640     EXPECT_EQ(added_songs[1].song_id(), "song6");
641 
642   }
643 
644 }
645 
646 } // namespace
647