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