Revision control

Copy as Markdown

// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <qmockintegration.h>
#include <QtTest/qtest.h>
#include <QtMultimedia/private/qsamplecache_p.h>
#include <QtCore/qendian.h>
#include <QtCore/qfile.h>
#include <QtCore/qfuturewatcher.h>
#include <cstdint>
Q_ENABLE_MOCK_MULTIMEDIA_PLUGIN
class tst_QSampleCache : public QObject
{
Q_OBJECT
public:
public slots:
private slots:
void initTestCase();
void cleanup();
void testCachedSample_data() { generateTestData(); }
void testCachedSample();
void testNotCachedSample_data() { generateTestData(); }
void testNotCachedSample();
void testInvalidFile_data() { generateTestData(); }
void testInvalidFile();
void testIncompatibleFile_data() { generateTestData(); }
void testIncompatibleFile();
void testDRwavHeapBufferOverflow_data() { generateTestData(); }
void testDRwavHeapBufferOverflow();
void testDRwavIntegerUnderflow_data() { generateTestData(); }
void testDRwavIntegerUnderflow();
void testLoadSampleFromSpan_valid();
void testLoadSampleFromSpan_invalid();
void testLoadSampleFromSpan_declaredFrameCountMismatch_data();
void testLoadSampleFromSpan_declaredFrameCountMismatch();
void testLoadSampleFromSpan_multiChannel();
void testLoadSampleViaDecoderBuffer_declaredFrameCountMismatch_data()
{
testLoadSampleFromSpan_declaredFrameCountMismatch_data();
}
void testLoadSampleViaDecoderBuffer_declaredFrameCountMismatch();
void testLoadSampleViaDecoder_valid();
void testLoadSampleViaDecoder_fallbackToDrWav();
void testLoadSampleViaDecoderBuffer_valid();
void testLoadSampleViaDecoderBuffer_fallbackToDrWav();
void testFallbackToDecoder_nonWav();
void testForcedDecoderPath();
void testFallbackToDecoder_notSupported();
void testMP3FallbackViaDecoder();
void testMP3FallbackViaDecoderBuffer();
void testLoadSampleAsyncViaDecoder_valid();
void testLoadSampleAsyncViaDecoder_fallbackToDrWav();
void testForcedDecoderPathAsync();
private:
template <typename T>
static void appendLittleEndian(QByteArray &out, T value)
{
const T le = qToLittleEndian(value);
out.append(reinterpret_cast<const char *>(&le), sizeof(le));
}
static QByteArray makeRf64Wav(uint64_t declaredFrameCount, uint16_t channelCount,
qsizetype dataBytes)
{
const uint16_t bytesPerFrame = channelCount * sizeof(int16_t);
QByteArray wav;
wav.append("RF64");
appendLittleEndian<uint32_t>(wav, 0xFFFFFFFF);
wav.append("WAVE");
wav.append("ds64");
appendLittleEndian<uint32_t>(wav, 28);
appendLittleEndian<uint64_t>(wav, 0); // RIFF size, ignored
appendLittleEndian<uint64_t>(wav, uint64_t(dataBytes));
appendLittleEndian<uint64_t>(wav, declaredFrameCount);
appendLittleEndian<uint32_t>(wav, 0); // table length
wav.append("fmt ");
appendLittleEndian<uint32_t>(wav, 16);
appendLittleEndian<uint16_t>(wav, 1); // PCM
appendLittleEndian<uint16_t>(wav, channelCount);
appendLittleEndian<uint32_t>(wav, 8000);
appendLittleEndian<uint32_t>(wav, 8000 * bytesPerFrame);
appendLittleEndian<uint16_t>(wav, bytesPerFrame);
appendLittleEndian<uint16_t>(wav, 16);
wav.append("data");
appendLittleEndian<uint32_t>(wav, 0xFFFFFFFF);
wav.append(QByteArray(dataBytes, '\x7f'));
return wav;
}
void generateTestData()
{
QTest::addColumn<QSampleCache::SampleSourceType>("sampleSourceType");
#ifdef QT_FEATURE_network
QTest::newRow("NetworkManager") << QSampleCache::SampleSourceType::NetworkManager;
#endif
QTest::newRow("File") << QSampleCache::SampleSourceType::File;
}
SharedSamplePtr requestSample(QSampleCache &cache, const QUrl &url,
std::optional<QSampleCache::SampleSourceType> sourceType = std::nullopt)
{
auto future = cache.requestSampleFuture(url, std::nullopt, sourceType);
QFutureWatcher<SharedSamplePtr> watcher;
watcher.setFuture(future);
QEventLoop loop;
connect(&watcher, &QFutureWatcher<SharedSamplePtr>::finished, &loop, [&] {
loop.exit(0);
});
loop.exec(QEventLoop::EventLoopExec);
return future.result();
}
};
void tst_QSampleCache::initTestCase()
{
// Ensure mock flags start clean
QMockIntegration::instance()->setFlags({});
}
void tst_QSampleCache::cleanup()
{
// Reset mock flags after each test
QMockIntegration::instance()->setFlags({});
}
void tst_QSampleCache::testCachedSample()
{
QFETCH(const QSampleCache::SampleSourceType, sampleSourceType);
QSampleCache cache;
SharedSamplePtr sample =
requestSample(cache, QUrl::fromLocalFile(QFINDTESTDATA("testdata/test.wav")), sampleSourceType);
QVERIFY(sample);
SharedSamplePtr sampleCached =
requestSample(cache, QUrl::fromLocalFile(QFINDTESTDATA("testdata/test.wav")), sampleSourceType);
QCOMPARE(sample, sampleCached); // sample is cached
QVERIFY(cache.isCached(QUrl::fromLocalFile(QFINDTESTDATA("testdata/test.wav"))));
}
void tst_QSampleCache::testNotCachedSample()
{
QFETCH(const QSampleCache::SampleSourceType, sampleSourceType);
QSampleCache cache;
SharedSamplePtr sample =
requestSample(cache, QUrl::fromLocalFile(QFINDTESTDATA("testdata/test.wav")), sampleSourceType);
QVERIFY(sample);
sample = {};
QVERIFY(!cache.isCached(QUrl::fromLocalFile(QFINDTESTDATA("testdata/test.wav"))));
}
void tst_QSampleCache::testInvalidFile()
{
QFETCH(const QSampleCache::SampleSourceType, sampleSourceType);
QSampleCache cache;
SharedSamplePtr sample = requestSample(cache, QUrl::fromLocalFile("invalid"), sampleSourceType);
QVERIFY(!sample);
sample = {};
QVERIFY(!cache.isCached(QUrl::fromLocalFile("invalid")));
}
void tst_QSampleCache::testIncompatibleFile()
{
QFETCH(const QSampleCache::SampleSourceType, sampleSourceType);
QSampleCache cache;
const QUrl corruptedWavUrl = QUrl::fromLocalFile(QFINDTESTDATA("testdata/corrupted.wav"));
SharedSamplePtr sample = requestSample(cache, corruptedWavUrl, sampleSourceType);
// sampleSourceType is set → fallback disabled, drwav fails → null
QVERIFY(!sample);
}
void tst_QSampleCache::testDRwavHeapBufferOverflow()
{
QFETCH(const QSampleCache::SampleSourceType, sampleSourceType);
QSampleCache cache;
const QUrl corruptedWavUrl =
QUrl::fromLocalFile(QFINDTESTDATA("testdata/drwav_heap-buffer-overflow.wav"));
SharedSamplePtr sample = requestSample(cache, corruptedWavUrl, sampleSourceType);
QVERIFY(sample); // we can still read it
}
void tst_QSampleCache::testDRwavIntegerUnderflow()
{
QFETCH(const QSampleCache::SampleSourceType, sampleSourceType);
QSampleCache cache;
const QUrl corruptedWavUrl =
QUrl::fromLocalFile(QFINDTESTDATA("testdata/drwav_integer-underflow.wav"));
SharedSamplePtr sample = requestSample(cache, corruptedWavUrl, sampleSourceType);
QVERIFY(!sample); // bad file
}
void tst_QSampleCache::testLoadSampleFromSpan_valid()
{
QFile file(QFINDTESTDATA("testdata/test.wav"));
QVERIFY(file.open(QFile::ReadOnly));
QByteArray data = file.readAll();
QVERIFY(!data.isEmpty());
auto result = QSampleCache::loadSample(data);
QVERIFY(result.has_value());
QCOMPARE(result->second.sampleFormat(), QAudioFormat::Float);
QVERIFY(result->second.sampleRate() > 0);
QVERIFY(!result->first.isEmpty());
}
void tst_QSampleCache::testLoadSampleFromSpan_invalid()
{
QByteArray garbage(100, '\xFF');
auto result = QSampleCache::loadSample(garbage);
QVERIFY(!result.has_value());
QCOMPARE(result.error(), QSampleLoadError::FormatError);
}
void tst_QSampleCache::testLoadSampleFromSpan_declaredFrameCountMismatch_data()
{
QTest::addColumn<uint64_t>("declaredFrameCount");
// 4 * (2^62 + 256) wraps to 1024 in 64-bit arithmetic
QTest::newRow("wrapping") << ((uint64_t(1) << 62) + 256);
QTest::newRow("huge") << (uint64_t(1) << 40);
QTest::newRow("more-than-data") << uint64_t(1024 * 1024);
}
void tst_QSampleCache::testLoadSampleFromSpan_declaredFrameCountMismatch()
{
QFETCH(const uint64_t, declaredFrameCount);
const QByteArray wav = makeRf64Wav(declaredFrameCount, 1, 1024 * 1024);
auto result = QSampleCache::loadSample(wav);
QVERIFY(!result.has_value());
QCOMPARE(result.error(), QSampleLoadError::FormatError);
}
void tst_QSampleCache::testLoadSampleFromSpan_multiChannel()
{
constexpr uint16_t channelCount = 2;
constexpr uint64_t frameCount = 3'000'000; // spans multiple decode chunks
const QByteArray wav =
makeRf64Wav(frameCount, channelCount, qsizetype(frameCount * channelCount * 2));
auto result = QSampleCache::loadSample(wav);
QVERIFY(result.has_value());
QCOMPARE_EQ(result->second.channelCount(), int(channelCount));
QCOMPARE_EQ(result->first.size(), qsizetype(frameCount * channelCount * sizeof(float)));
}
void tst_QSampleCache::testLoadSampleViaDecoderBuffer_declaredFrameCountMismatch()
{
QFETCH(const uint64_t, declaredFrameCount);
QMockIntegration::instance()->setFlags(QMockIntegration::NoAudioDecoderInterface);
const QByteArray wav = makeRf64Wav(declaredFrameCount, 1, 1024 * 1024);
auto result = QSampleCache::loadSampleViaDecoder(wav);
QVERIFY(!result.has_value());
}
void tst_QSampleCache::testLoadSampleViaDecoder_valid()
{
const QUrl url = QUrl::fromLocalFile(QFINDTESTDATA("testdata/test.wav"));
auto result = QSampleCache::loadSampleViaDecoder(url);
QVERIFY(result.has_value());
QCOMPARE(result->second.sampleFormat(), QAudioFormat::Float);
QVERIFY(!result->first.isEmpty());
QVERIFY(result->second.sampleRate() > 0);
QVERIFY(result->second.channelCount() > 0);
}
void tst_QSampleCache::testLoadSampleViaDecoder_fallbackToDrWav()
{
QMockIntegration::instance()->setFlags(QMockIntegration::NoAudioDecoderInterface);
const QUrl url = QUrl::fromLocalFile(QFINDTESTDATA("testdata/nokia-tune.mp3"));
auto result = QSampleCache::loadSampleViaDecoder(url);
QVERIFY(!result.has_value());
QCOMPARE(result.error(), QSampleLoadError::DecoderError);
}
void tst_QSampleCache::testLoadSampleViaDecoderBuffer_valid()
{
QFile file(QFINDTESTDATA("testdata/test.wav"));
QVERIFY(file.open(QFile::ReadOnly));
QByteArray data = file.readAll();
QVERIFY(!data.isEmpty());
auto result = QSampleCache::loadSampleViaDecoder(data);
QVERIFY(result.has_value());
QCOMPARE(result->second.sampleFormat(), QAudioFormat::Float);
QVERIFY(!result->first.isEmpty());
QVERIFY(result->second.sampleRate() > 0);
QVERIFY(result->second.channelCount() > 0);
}
void tst_QSampleCache::testLoadSampleViaDecoderBuffer_fallbackToDrWav()
{
QMockIntegration::instance()->setFlags(QMockIntegration::NoAudioDecoderInterface);
QFile file(QFINDTESTDATA("testdata/nokia-tune.mp3"));
QVERIFY(file.open(QFile::ReadOnly));
QByteArray data = file.readAll();
auto result = QSampleCache::loadSampleViaDecoder(data);
QVERIFY(!result.has_value());
QCOMPARE(result.error(), QSampleLoadError::DecoderError);
}
void tst_QSampleCache::testFallbackToDecoder_nonWav()
{
// No sampleSourceType → fallback enabled
QSampleCache cache;
const QUrl url = QUrl::fromLocalFile(QFINDTESTDATA("testdata/corrupted.wav"));
SharedSamplePtr sample = requestSample(cache, url);
// drwav fails, mock decoder succeeds → sample ready
QVERIFY(sample);
QCOMPARE(sample->state(), QSample::Ready);
}
void tst_QSampleCache::testForcedDecoderPath()
{
QSampleCache cache;
const QUrl url = QUrl::fromLocalFile(QFINDTESTDATA("testdata/test.wav"));
SharedSamplePtr sample = requestSample(cache, url, QSampleCache::SampleSourceType::AudioDecoder);
// AudioDecoder skips drwav, uses mock decoder directly
QVERIFY(sample);
QCOMPARE(sample->state(), QSample::Ready);
QCOMPARE(sample->format().sampleFormat(), QAudioFormat::Float);
}
void tst_QSampleCache::testFallbackToDecoder_notSupported()
{
QMockIntegration::instance()->setFlags(QMockIntegration::NoAudioDecoderInterface);
// No sampleSourceType → fallback enabled; drwav fallback via QAudioDecoder
// still fails because the file itself is corrupted, not because a
// platform decoder is unavailable.
QSampleCache cache;
const QUrl url = QUrl::fromLocalFile(QFINDTESTDATA("testdata/corrupted.wav"));
SharedSamplePtr sample = requestSample(cache, url);
// drwav fails, decoder fallback also fails on the corrupted data → null
QVERIFY(!sample);
}
void tst_QSampleCache::testMP3FallbackViaDecoder()
{
// No sampleSourceType → fallback enabled
QSampleCache cache;
const QUrl url = QUrl::fromLocalFile(QFINDTESTDATA("testdata/nokia-tune.mp3"));
SharedSamplePtr sample = requestSample(cache, url);
// drwav fails on MP3, mock decoder succeeds → sample ready
QVERIFY(sample);
QCOMPARE(sample->state(), QSample::Ready);
QCOMPARE(sample->format().sampleFormat(), QAudioFormat::Float);
// Verify decoder was actually invoked via the mock
QVERIFY(QMockIntegration::instance()->lastAudioDecoder() != nullptr);
}
void tst_QSampleCache::testMP3FallbackViaDecoderBuffer()
{
QFile file(QFINDTESTDATA("testdata/nokia-tune.mp3"));
QVERIFY(file.open(QFile::ReadOnly));
QByteArray data = file.readAll();
QVERIFY(!data.isEmpty());
auto result = QSampleCache::loadSampleViaDecoder(data);
// drwav fails, mock decoder succeeds via QBuffer
QVERIFY(result.has_value());
QCOMPARE(result->second.sampleFormat(), QAudioFormat::Float);
QVERIFY(!result->first.isEmpty());
}
void tst_QSampleCache::testLoadSampleAsyncViaDecoder_valid()
{
QFile file(QFINDTESTDATA("testdata/test.wav"));
QVERIFY(file.open(QFile::ReadOnly));
QByteArray data = file.readAll();
QVERIFY(!data.isEmpty());
auto future = QSampleCache::loadSampleAsyncViaDecoder(data);
QTRY_VERIFY(future.isFinished());
auto result = future.result();
QVERIFY(result.has_value());
QCOMPARE(result->second.sampleFormat(), QAudioFormat::Float);
QVERIFY(!result->first.isEmpty());
QVERIFY(result->second.sampleRate() > 0);
QVERIFY(result->second.channelCount() > 0);
QVERIFY(QMockIntegration::instance()->lastAudioDecoder() != nullptr);
}
void tst_QSampleCache::testLoadSampleAsyncViaDecoder_fallbackToDrWav()
{
QMockIntegration::instance()->setFlags(QMockIntegration::NoAudioDecoderInterface);
QFile file(QFINDTESTDATA("testdata/nokia-tune.mp3"));
QVERIFY(file.open(QFile::ReadOnly));
QByteArray data = file.readAll();
auto future = QSampleCache::loadSampleAsyncViaDecoder(data);
QTRY_VERIFY(future.isFinished());
auto result = future.result();
QVERIFY(!result.has_value());
QCOMPARE(result.error(), QSampleLoadError::DecoderError);
}
void tst_QSampleCache::testForcedDecoderPathAsync()
{
QSampleCache cache;
const QUrl url = QUrl::fromLocalFile(QFINDTESTDATA("testdata/test.wav"));
SharedSamplePtr sample = requestSample(cache, url, QSampleCache::SampleSourceType::AudioDecoder);
QVERIFY(sample);
QCOMPARE(sample->state(), QSample::Ready);
QCOMPARE(sample->format().sampleFormat(), QAudioFormat::Float);
}
QTEST_GUILESS_MAIN(tst_QSampleCache)
#include "tst_qsamplecache.moc"