1 #include "track/globaltrackcache.h"
2 
3 #include <QThread>
4 #include <QtDebug>
5 #include <atomic>
6 
7 #include "test/mixxxtest.h"
8 #include "track/track.h"
9 
10 namespace {
11 
12 const QDir kTestDir(QDir::current().absoluteFilePath("src/test/id3-test-data"));
13 
14 const TrackFile kTestFile(kTestDir.absoluteFilePath("cover-test.flac"));
15 const TrackFile kTestFile2(kTestDir.absoluteFilePath("cover-test.ogg"));
16 
17 class TrackTitleThread: public QThread {
18   public:
TrackTitleThread()19     explicit TrackTitleThread()
20         : m_stop(false) {
21     }
22 
stop()23     void stop() {
24         m_stop.store(true);
25     }
26 
run()27     void run() override {
28         int loopCount = 0;
29         while (!(m_stop.load() && GlobalTrackCacheLocker().isEmpty())) {
30             // Drop the previous reference to avoid resolving the
31             // same track twice
32             m_recentTrackPtr.reset();
33             // Try to resolve the next track by guessing the id
34             const TrackId trackId(loopCount % 2);
35             auto track = GlobalTrackCacheLocker().lookupTrackById(trackId);
36             if (track) {
37                 ASSERT_EQ(trackId, track->getId());
38                 // lp1744550: Accessing the track from multiple threads is
39                 // required to cause a SIGSEGV
40                 if (track->getTitle().isEmpty()) {
41                     track->setTitle(
42                             QString("Title %1").arg(QString::number(loopCount)));
43                 } else {
44                     track->setTitle(QString());
45                 }
46                 ASSERT_TRUE(track->isDirty());
47             }
48             // Replace the current reference with this one and keep it alive
49             // until the next loop cycle
50             m_recentTrackPtr = std::move(track);
51             ++loopCount;
52         }
53         // If the cache is empty all references must have been dropped.
54         // Why? m_recentTrackPtr is only valid if a pointer has been found
55         // in the cache during the previous cycle, i.e. the cache could not
56         // have been empty. In this case at least another loop cycle follow,
57         // and so on...
58         ASSERT_TRUE(!m_recentTrackPtr);
59         qDebug() << "Finished" << loopCount << " thread loops";
60     }
61 
62   private:
63     TrackPointer m_recentTrackPtr;
64 
65     std::atomic<bool> m_stop;
66 };
67 
deleteTrack(Track * pTrack)68 void deleteTrack(Track* pTrack) {
69     // Delete track objects directly in unit tests with
70     // no main event loop
71     delete pTrack;
72 };
73 
74 } // anonymous namespace
75 
76 class GlobalTrackCacheTest: public MixxxTest, public virtual GlobalTrackCacheSaver {
77   public:
saveEvictedTrack(Track * pTrack)78     void saveEvictedTrack(Track* pTrack) noexcept override {
79         ASSERT_FALSE(pTrack == nullptr);
80     }
81 
82   protected:
GlobalTrackCacheTest()83     GlobalTrackCacheTest() {
84         GlobalTrackCache::createInstance(this, deleteTrack);
85     }
~GlobalTrackCacheTest()86     ~GlobalTrackCacheTest() {
87         GlobalTrackCache::destroyInstance();
88     }
89 
90     TrackPointer m_recentTrackPtr;
91 };
92 
TEST_F(GlobalTrackCacheTest,resolveByFileInfo)93 TEST_F(GlobalTrackCacheTest, resolveByFileInfo) {
94     ASSERT_TRUE(GlobalTrackCacheLocker().isEmpty());
95 
96     const TrackId trackId(1);
97 
98     TrackPointer track;
99     {
100         GlobalTrackCacheResolver resolver(kTestFile);
101         track = resolver.getTrack();
102         EXPECT_TRUE(static_cast<bool>(track));
103         EXPECT_EQ(2, track.use_count());
104 
105         resolver.initTrackIdAndUnlockCache(trackId);
106     }
107     EXPECT_EQ(1, track.use_count());
108 
109     TrackWeakPointer trackWeak(track);
110     EXPECT_EQ(1, trackWeak.use_count());
111 
112     TrackPointer trackCopy = track;
113     EXPECT_EQ(2, trackCopy.use_count());
114     EXPECT_EQ(2, track.use_count());
115     EXPECT_EQ(2, trackWeak.use_count());
116 
117     trackCopy.reset();
118     EXPECT_EQ(1, track.use_count());
119     EXPECT_EQ(1, trackWeak.use_count());
120 
121     auto trackById = GlobalTrackCacheLocker().lookupTrackById(trackId);
122     EXPECT_EQ(track, trackById);
123     EXPECT_EQ(2, trackById.use_count());
124     EXPECT_EQ(2, track.use_count());
125     EXPECT_EQ(2, trackWeak.use_count());
126 
127     trackById.reset();
128     EXPECT_EQ(1, trackWeak.use_count());
129     EXPECT_EQ(track, TrackPointer(trackWeak.lock()));
130 
131     track.reset();
132     EXPECT_EQ(0, trackWeak.use_count());
133     EXPECT_EQ(TrackPointer(), TrackPointer(trackWeak.lock()));
134 
135     {
136         GlobalTrackCacheLocker cacheLocker;
137         trackById = cacheLocker.lookupTrackById(trackId);
138         EXPECT_EQ(TrackPointer(), trackById);
139         EXPECT_TRUE(cacheLocker.isEmpty());
140     }
141 }
142 
TEST_F(GlobalTrackCacheTest,concurrentDelete)143 TEST_F(GlobalTrackCacheTest, concurrentDelete) {
144     ASSERT_TRUE(GlobalTrackCacheLocker().isEmpty());
145 
146     TrackTitleThread workerThread;
147     workerThread.start();
148 
149     // lp1744550: A decent number of iterations is needed to reliably
150     // reveal potential race conditions while evicting tracks from
151     // the cache!
152     // NOTE(2019-12-14, uklotzde): On Travis and macOS executing 10_000
153     // iterations takes ~1 sec. In order to safely finish this test within
154     // the timeout limit of 30 sec. we use 20 * 10_000 = 200_000 iterations.
155     for (int i = 0; i < 200000; ++i) {
156         m_recentTrackPtr.reset();
157 
158         TrackId trackId;
159 
160         TrackPointer track;
161         {
162             GlobalTrackCacheResolver resolver(kTestFile);
163             track = resolver.getTrack();
164             EXPECT_TRUE(static_cast<bool>(track));
165             trackId = track->getId();
166             if (!trackId.isValid()) {
167                 trackId = TrackId(i % 2);
168                 resolver.initTrackIdAndUnlockCache(trackId);
169             }
170         }
171 
172         track = GlobalTrackCacheLocker().lookupTrackById(trackId);
173         EXPECT_TRUE(static_cast<bool>(track));
174 
175         // lp1744550: Accessing the track from multiple threads is
176         // required to cause a SIGSEGV
177         track->setArtist(QString("Artist %1").arg(QString::number(i)));
178 
179         m_recentTrackPtr = std::move(track);
180 
181         // Lookup the track again
182         track = GlobalTrackCacheLocker().lookupTrackById(trackId);
183         EXPECT_TRUE(static_cast<bool>(track));
184 
185         // Ensure that track objects are evicted and deleted
186         QCoreApplication::processEvents();
187     }
188     m_recentTrackPtr.reset();
189 
190     workerThread.stop();
191 
192     // Ensure that all track objects have been deleted
193     while (!GlobalTrackCacheLocker().isEmpty()) {
194         QCoreApplication::processEvents();
195     }
196 
197     workerThread.wait();
198 }
199 
TEST_F(GlobalTrackCacheTest,evictWhileMoving)200 TEST_F(GlobalTrackCacheTest, evictWhileMoving) {
201     ASSERT_TRUE(GlobalTrackCacheLocker().isEmpty());
202 
203     TrackPointer track1 = GlobalTrackCacheResolver(kTestFile).getTrack();
204     EXPECT_TRUE(static_cast<bool>(track1));
205 
206     TrackPointer track2 = GlobalTrackCacheResolver(kTestFile2).getTrack();
207     EXPECT_TRUE(static_cast<bool>(track2));
208 
209     track1 = std::move(track2);
210 
211     EXPECT_TRUE(static_cast<bool>(track1));
212     EXPECT_FALSE(static_cast<bool>(track2));
213 
214     track1.reset();
215 
216     EXPECT_TRUE(GlobalTrackCacheLocker().isEmpty());
217 }
218