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