blob: fa2d5e37234104b1c679799a347b4d08df61140c [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include <QDebug>
#include <QFile>
#if QT_CONFIG(process)
# include <QProcess>
#endif
#include <QSharedMemory>
#include <QTest>
#include <QThread>
#include <QElapsedTimer>
#define EXISTING_SHARE "existing"
#define EXISTING_SIZE 1024
Q_DECLARE_METATYPE(QSharedMemory::SharedMemoryError)
Q_DECLARE_METATYPE(QSharedMemory::AccessMode)
class tst_QSharedMemory : public QObject
{
Q_OBJECT
public:
tst_QSharedMemory();
virtual ~tst_QSharedMemory();
public Q_SLOTS:
void init();
void cleanup();
private slots:
// basics
void constructor();
void key_data();
void key();
void create_data();
void create();
void attach_data();
void attach();
void lock();
// custom edge cases
#ifndef Q_OS_HPUX
void removeWhileAttached();
#endif
void emptyMemory();
#if !defined(Q_OS_WIN)
void readOnly();
#endif
// basics all together
#ifndef Q_OS_HPUX
void simpleProducerConsumer_data();
void simpleProducerConsumer();
void simpleDoubleProducerConsumer();
#endif
// with threads
void simpleThreadedProducerConsumer_data();
void simpleThreadedProducerConsumer();
// with processes
void simpleProcessProducerConsumer_data();
void simpleProcessProducerConsumer();
// extreme cases
void useTooMuchMemory();
#if !defined(Q_OS_HPUX)
void attachTooMuch();
#endif
// unique keys
void uniqueKey_data();
void uniqueKey();
protected:
int remove(const QString &key);
QString rememberKey(const QString &key)
{
if (key == EXISTING_SHARE)
return key;
if (!keys.contains(key)) {
keys.append(key);
remove(key);
}
return key;
}
QStringList keys;
QList<QSharedMemory*> jail;
QSharedMemory *existingSharedMemory;
private:
const QString m_helperBinary;
};
tst_QSharedMemory::tst_QSharedMemory()
: existingSharedMemory(0)
, m_helperBinary("producerconsumer_helper")
{
}
tst_QSharedMemory::~tst_QSharedMemory()
{
}
void tst_QSharedMemory::init()
{
existingSharedMemory = new QSharedMemory(EXISTING_SHARE);
if (!existingSharedMemory->create(EXISTING_SIZE)) {
QCOMPARE(existingSharedMemory->error(), QSharedMemory::AlreadyExists);
}
}
void tst_QSharedMemory::cleanup()
{
delete existingSharedMemory;
qDeleteAll(jail.begin(), jail.end());
jail.clear();
keys.append(EXISTING_SHARE);
for (int i = 0; i < keys.count(); ++i) {
QSharedMemory sm(keys.at(i));
if (!sm.create(1024)) {
//if (sm.error() != QSharedMemory::KeyError)
// qWarning() << "test cleanup: remove failed:" << keys.at(i) << sm.error() << sm.errorString();
sm.attach();
sm.detach();
remove(keys.at(i));
}
}
}
#ifndef Q_OS_WIN
#include <private/qsharedmemory_p.h>
#include <sys/types.h>
#ifndef QT_POSIX_IPC
#include <sys/ipc.h>
#include <sys/shm.h>
#else
#include <sys/mman.h>
#endif // QT_POSIX_IPC
#include <errno.h>
#endif
int tst_QSharedMemory::remove(const QString &key)
{
#ifdef Q_OS_WIN
Q_UNUSED(key);
return 0;
#else
// On unix the shared memory might exists from a previously failed test
// or segfault, remove it it does
if (key.isEmpty())
return -1;
// ftok requires that an actual file exists somewhere
QString fileName = QSharedMemoryPrivate::makePlatformSafeKey(key);
if (!QFile::exists(fileName)) {
//qDebug() << "exits failed";
return -2;
}
#ifndef QT_POSIX_IPC
int unix_key = ftok(fileName.toLatin1().constData(), 'Q');
if (-1 == unix_key) {
qDebug() << "ftok failed";
return -3;
}
int id = shmget(unix_key, 0, 0600);
if (-1 == id) {
qDebug() << "shmget failed" << strerror(errno);
return -4;
}
struct shmid_ds shmid_ds;
if (-1 == shmctl(id, IPC_RMID, &shmid_ds)) {
qDebug() << "shmctl failed";
return -5;
}
#else
if (shm_unlink(QFile::encodeName(fileName).constData()) == -1) {
qDebug() << "shm_unlink failed";
return -5;
}
#endif // QT_POSIX_IPC
return QFile::remove(fileName);
#endif // Q_OS_WIN
}
/*!
Tests the default values
*/
void tst_QSharedMemory::constructor()
{
QSharedMemory sm;
QCOMPARE(sm.key(), QString());
QVERIFY(!sm.isAttached());
QVERIFY(!sm.data());
QCOMPARE(sm.size(), 0);
QCOMPARE(sm.error(), QSharedMemory::NoError);
QCOMPARE(sm.errorString(), QString());
}
void tst_QSharedMemory::key_data()
{
QTest::addColumn<QString>("constructorKey");
QTest::addColumn<QString>("setKey");
QTest::addColumn<QString>("setNativeKey");
QTest::newRow("null, null, null") << QString() << QString() << QString();
QTest::newRow("one, null, null") << QString("one") << QString() << QString();
QTest::newRow("null, one, null") << QString() << QString("one") << QString();
QTest::newRow("null, null, one") << QString() << QString() << QString("one");
QTest::newRow("one, two, null") << QString("one") << QString("two") << QString();
QTest::newRow("one, null, two") << QString("one") << QString() << QString("two");
QTest::newRow("null, one, two") << QString() << QString("one") << QString("two");
QTest::newRow("one, two, three") << QString("one") << QString("two") << QString("three");
QTest::newRow("invalid") << QString("o/e") << QString("t/o") << QString("|x");
}
/*!
Basic key testing
*/
void tst_QSharedMemory::key()
{
QFETCH(QString, constructorKey);
QFETCH(QString, setKey);
QFETCH(QString, setNativeKey);
QSharedMemory sm(constructorKey);
QCOMPARE(sm.key(), constructorKey);
QCOMPARE(sm.nativeKey().isEmpty(), constructorKey.isEmpty());
sm.setKey(setKey);
QCOMPARE(sm.key(), setKey);
QCOMPARE(sm.nativeKey().isEmpty(), setKey.isEmpty());
sm.setNativeKey(setNativeKey);
QVERIFY(sm.key().isNull());
QCOMPARE(sm.nativeKey(), setNativeKey);
QCOMPARE(sm.isAttached(), false);
QCOMPARE(sm.error(), QSharedMemory::NoError);
QCOMPARE(sm.errorString(), QString());
QVERIFY(!sm.data());
QCOMPARE(sm.size(), 0);
QCOMPARE(sm.detach(), false);
}
void tst_QSharedMemory::create_data()
{
QTest::addColumn<QString>("key");
QTest::addColumn<int>("size");
QTest::addColumn<bool>("canCreate");
QTest::addColumn<QSharedMemory::SharedMemoryError>("error");
QTest::newRow("null key") << QString() << 1024
<< false << QSharedMemory::KeyError;
QTest::newRow("-1 size") << QString("negsize") << -1
<< false << QSharedMemory::InvalidSize;
QTest::newRow("nor size") << QString("norsize") << 1024
<< true << QSharedMemory::NoError;
QTest::newRow("already exists") << QString(EXISTING_SHARE) << EXISTING_SIZE
<< false << QSharedMemory::AlreadyExists;
}
/*!
Basic create testing
*/
void tst_QSharedMemory::create()
{
QFETCH(QString, key);
QFETCH(int, size);
QFETCH(bool, canCreate);
QFETCH(QSharedMemory::SharedMemoryError, error);
QSharedMemory sm(rememberKey(key));
QCOMPARE(sm.create(size), canCreate);
if (sm.error() != error)
qDebug() << sm.errorString();
QCOMPARE(sm.key(), key);
if (canCreate) {
QCOMPARE(sm.errorString(), QString());
QVERIFY(sm.data() != 0);
QVERIFY(sm.size() != 0);
} else {
QVERIFY(!sm.data());
QVERIFY(sm.errorString() != QString());
}
}
void tst_QSharedMemory::attach_data()
{
QTest::addColumn<QString>("key");
QTest::addColumn<bool>("exists");
QTest::addColumn<QSharedMemory::SharedMemoryError>("error");
QTest::newRow("null key") << QString() << false << QSharedMemory::KeyError;
QTest::newRow("doesn't exists") << QString("doesntexists") << false << QSharedMemory::NotFound;
// HPUX doesn't allow for multiple attaches per process.
#ifndef Q_OS_HPUX
QTest::newRow("already exists") << QString(EXISTING_SHARE) << true << QSharedMemory::NoError;
#endif
}
/*!
Basic attach/detach testing
*/
void tst_QSharedMemory::attach()
{
QFETCH(QString, key);
QFETCH(bool, exists);
QFETCH(QSharedMemory::SharedMemoryError, error);
QSharedMemory sm(key);
QCOMPARE(sm.attach(), exists);
QCOMPARE(sm.isAttached(), exists);
QCOMPARE(sm.error(), error);
QCOMPARE(sm.key(), key);
if (exists) {
QVERIFY(sm.data() != 0);
QVERIFY(sm.size() != 0);
QCOMPARE(sm.errorString(), QString());
QVERIFY(sm.detach());
// Make sure detach doesn't screw up something and we can't re-attach.
QVERIFY(sm.attach());
QVERIFY(sm.data() != 0);
QVERIFY(sm.size() != 0);
QVERIFY(sm.detach());
QCOMPARE(sm.size(), 0);
QVERIFY(!sm.data());
} else {
QVERIFY(!sm.data());
QCOMPARE(sm.size(), 0);
QVERIFY(sm.errorString() != QString());
QVERIFY(!sm.detach());
}
}
void tst_QSharedMemory::lock()
{
QSharedMemory shm;
QVERIFY(!shm.lock());
QCOMPARE(shm.error(), QSharedMemory::LockError);
shm.setKey(QLatin1String("qsharedmemory"));
QVERIFY(!shm.lock());
QCOMPARE(shm.error(), QSharedMemory::LockError);
QVERIFY(shm.create(100));
QVERIFY(shm.lock());
QTest::ignoreMessage(QtWarningMsg, "QSharedMemory::lock: already locked");
QVERIFY(shm.lock());
// we didn't unlock(), so ignore the warning from auto-detach in destructor
QTest::ignoreMessage(QtWarningMsg, "QSharedMemory::lock: already locked");
}
/*!
Other shared memory are allowed to be attached after we remove,
but new shared memory are not allowed to attach after a remove.
*/
// HPUX doesn't allow for multiple attaches per process.
#ifndef Q_OS_HPUX
void tst_QSharedMemory::removeWhileAttached()
{
rememberKey("one");
// attach 1
QSharedMemory *smOne = new QSharedMemory(QLatin1String("one"));
QVERIFY(smOne->create(1024));
QVERIFY(smOne->isAttached());
// attach 2
QSharedMemory *smTwo = new QSharedMemory(QLatin1String("one"));
QVERIFY(smTwo->attach());
QVERIFY(smTwo->isAttached());
// detach 1 and remove, remove one first to catch another error.
delete smOne;
delete smTwo;
// three shouldn't be able to attach
QSharedMemory smThree(QLatin1String("one"));
QVERIFY(!smThree.attach());
QCOMPARE(smThree.error(), QSharedMemory::NotFound);
}
#endif
/*!
The memory should be set to 0 after created.
*/
void tst_QSharedMemory::emptyMemory()
{
QSharedMemory sm(rememberKey(QLatin1String("voidland")));
int size = 1024;
QVERIFY(sm.create(size, QSharedMemory::ReadOnly));
char *get = (char*)sm.data();
char null = 0;
for (int i = 0; i < size; ++i)
QCOMPARE(get[i], null);
}
/*!
Verify that attach with ReadOnly is actually read only
by writing to data and causing a segfault.
*/
// This test opens a crash dialog on Windows.
#if !defined(Q_OS_WIN)
void tst_QSharedMemory::readOnly()
{
#if !QT_CONFIG(process)
QSKIP("No qprocess support", SkipAll);
#elif defined(Q_OS_MACOS)
QSKIP("QTBUG-59936: Times out on macOS", SkipAll);
#else
rememberKey("readonly_segfault");
// ### on windows disable the popup somehow
QProcess p;
p.start(m_helperBinary, QStringList("readonly_segfault"));
p.setProcessChannelMode(QProcess::ForwardedChannels);
p.waitForFinished();
QCOMPARE(p.error(), QProcess::Crashed);
#endif
}
#endif
/*!
Keep making shared memory until the kernel stops us.
*/
void tst_QSharedMemory::useTooMuchMemory()
{
#ifdef Q_OS_LINUX
bool success = true;
int count = 0;
while (success) {
QString key = QLatin1String("maxmemorytest_") + QString::number(count++);
QSharedMemory *sm = new QSharedMemory(rememberKey(key));
QVERIFY(sm);
jail.append(sm);
int size = 32768 * 1024;
success = sm->create(size);
if (!success && sm->error() == QSharedMemory::AlreadyExists) {
// left over from a crash, clean it up
sm->attach();
sm->detach();
success = sm->create(size);
}
if (!success) {
QVERIFY(!sm->isAttached());
QCOMPARE(sm->key(), key);
QCOMPARE(sm->size(), 0);
QVERIFY(!sm->data());
if (sm->error() != QSharedMemory::OutOfResources)
qDebug() << sm->error() << sm->errorString();
// ### Linux won't return OutOfResources if there are not enough semaphores to use.
QVERIFY(sm->error() == QSharedMemory::OutOfResources
|| sm->error() == QSharedMemory::LockError);
QVERIFY(sm->errorString() != QString());
QVERIFY(!sm->attach());
QVERIFY(!sm->detach());
} else {
QVERIFY(sm->isAttached());
}
}
#endif
}
/*!
Create one shared memory (government) and see how many other shared memories (wars) we can
attach before the system runs out of resources.
*/
// HPUX doesn't allow for multiple attaches per process.
#if !defined(Q_OS_HPUX)
void tst_QSharedMemory::attachTooMuch()
{
QSKIP("disabled");
QSharedMemory government(rememberKey("government"));
QVERIFY(government.create(1024));
while (true) {
QSharedMemory *war = new QSharedMemory(government.key());
QVERIFY(war);
jail.append(war);
if (!war->attach()) {
QVERIFY(!war->isAttached());
QCOMPARE(war->key(), government.key());
QCOMPARE(war->size(), 0);
QVERIFY(!war->data());
QCOMPARE(war->error(), QSharedMemory::OutOfResources);
QVERIFY(war->errorString() != QString());
QVERIFY(!war->detach());
break;
} else {
QVERIFY(war->isAttached());
}
}
}
#endif
// HPUX doesn't allow for multiple attaches per process.
#ifndef Q_OS_HPUX
void tst_QSharedMemory::simpleProducerConsumer_data()
{
QTest::addColumn<QSharedMemory::AccessMode>("mode");
QTest::newRow("readonly") << QSharedMemory::ReadOnly;
QTest::newRow("readwrite") << QSharedMemory::ReadWrite;
}
/*!
The basic consumer producer that rounds out the basic testing.
If this fails then any muli-threading/process might fail (but be
harder to debug)
This doesn't require nor test any locking system.
*/
void tst_QSharedMemory::simpleProducerConsumer()
{
QFETCH(QSharedMemory::AccessMode, mode);
rememberKey(QLatin1String("market"));
QSharedMemory producer(QLatin1String("market"));
QSharedMemory consumer(QLatin1String("market"));
int size = 512;
QVERIFY(producer.create(size));
QVERIFY(consumer.attach(mode));
char *put = (char*)producer.data();
char *get = (char*)consumer.data();
// On Windows CE you always have ReadWrite access. Thus
// ViewMapOfFile returns the same pointer
QVERIFY(put != get);
for (int i = 0; i < size; ++i) {
put[i] = 'Q';
QCOMPARE(get[i], 'Q');
}
QVERIFY(consumer.detach());
}
#endif
// HPUX doesn't allow for multiple attaches per process.
#ifndef Q_OS_HPUX
void tst_QSharedMemory::simpleDoubleProducerConsumer()
{
rememberKey(QLatin1String("market"));
QSharedMemory producer(QLatin1String("market"));
int size = 512;
QVERIFY(producer.create(size));
QVERIFY(producer.detach());
QVERIFY(producer.create(size));
{
QSharedMemory consumer(QLatin1String("market"));
QVERIFY(consumer.attach());
}
}
#endif
class Consumer : public QThread
{
public:
void run()
{
QSharedMemory consumer(QLatin1String("market"));
while (!consumer.attach()) {
if (consumer.error() != QSharedMemory::NotFound)
qDebug() << "consumer: failed to connect" << consumer.error() << consumer.errorString();
QVERIFY(consumer.error() == QSharedMemory::NotFound || consumer.error() == QSharedMemory::KeyError);
QTest::qWait(1);
}
char *memory = (char*)consumer.data();
int i = 0;
while (true) {
if (!consumer.lock())
break;
if (memory[0] == 'Q')
memory[0] = ++i;
if (memory[0] == 'E') {
memory[1]++;
QVERIFY(consumer.unlock());
break;
}
QVERIFY(consumer.unlock());
QTest::qWait(1);
}
QVERIFY(consumer.detach());
}
};
class Producer : public QThread
{
public:
Producer() : producer(QLatin1String("market"))
{
int size = 1024;
if (!producer.create(size)) {
// left over from a crash...
if (producer.error() == QSharedMemory::AlreadyExists) {
producer.attach();
producer.detach();
QVERIFY(producer.create(size));
}
}
}
void run()
{
char *memory = (char*)producer.data();
memory[1] = '0';
QElapsedTimer timer;
timer.start();
int i = 0;
while (i < 5 && timer.elapsed() < 5000) {
QVERIFY(producer.lock());
if (memory[0] == 'Q') {
QVERIFY(producer.unlock());
QTest::qWait(1);
continue;
}
++i;
memory[0] = 'Q';
QVERIFY(producer.unlock());
QTest::qWait(1);
}
// tell everyone to quit
QVERIFY(producer.lock());
memory[0] = 'E';
QVERIFY(producer.unlock());
}
QSharedMemory producer;
private:
};
void tst_QSharedMemory::simpleThreadedProducerConsumer_data()
{
QTest::addColumn<bool>("producerIsThread");
QTest::addColumn<int>("threads");
for (int i = 0; i < 5; ++i) {
QTest::newRow("1 consumer, producer is thread") << true << 1;
QTest::newRow("1 consumer, producer is this") << false << 1;
QTest::newRow("5 consumers, producer is thread") << true << 5;
QTest::newRow("5 consumers, producer is this") << false << 5;
}
}
/*!
The basic producer/consumer, but this time using threads.
*/
void tst_QSharedMemory::simpleThreadedProducerConsumer()
{
QFETCH(bool, producerIsThread);
QFETCH(int, threads);
rememberKey(QLatin1String("market"));
#if defined Q_OS_HPUX && defined __ia64
QSKIP("This test locks up on gravlaks.troll.no");
#endif
Producer p;
QVERIFY(p.producer.isAttached());
if (producerIsThread)
p.start();
QList<Consumer*> consumers;
for (int i = 0; i < threads; ++i) {
consumers.append(new Consumer());
consumers.last()->start();
}
if (!producerIsThread)
p.run();
p.wait(5000);
while (!consumers.isEmpty()) {
Consumer *c = consumers.first();
QVERIFY(c->isFinished() || c->wait(5000));
delete consumers.takeFirst();
}
}
void tst_QSharedMemory::simpleProcessProducerConsumer_data()
{
#if QT_CONFIG(process)
QTest::addColumn<int>("processes");
int tries = 5;
for (int i = 0; i < tries; ++i) {
QTest::newRow("1 process") << 1;
QTest::newRow("5 processes") << 5;
}
#endif
}
/*!
Create external processes that produce and consume.
*/
void tst_QSharedMemory::simpleProcessProducerConsumer()
{
#if !QT_CONFIG(process)
QSKIP("No qprocess support", SkipAll);
#else
QFETCH(int, processes);
QSKIP("This test is unstable: QTBUG-25655");
rememberKey("market");
QProcess producer;
producer.start(m_helperBinary, QStringList("producer"));
QVERIFY2(producer.waitForStarted(), "Could not start helper binary");
QVERIFY2(producer.waitForReadyRead(), "Helper process failed to create shared memory segment: " +
producer.readAllStandardError());
QList<QProcess*> consumers;
unsigned int failedProcesses = 0;
const QStringList consumerArguments = QStringList("consumer");
for (int i = 0; i < processes; ++i) {
QProcess *p = new QProcess;
p->setProcessChannelMode(QProcess::ForwardedChannels);
p->start(m_helperBinary, consumerArguments);
if (p->waitForStarted(2000))
consumers.append(p);
else
++failedProcesses;
}
bool consumerFailed = false;
while (!consumers.isEmpty()) {
QVERIFY(consumers.first()->waitForFinished(3000));
if (consumers.first()->state() == QProcess::Running ||
consumers.first()->exitStatus() != QProcess::NormalExit ||
consumers.first()->exitCode() != 0) {
consumerFailed = true;
}
delete consumers.takeFirst();
}
QCOMPARE(consumerFailed, false);
QCOMPARE(failedProcesses, (unsigned int)(0));
// tell the producer to exit now
producer.write("", 1);
producer.waitForBytesWritten();
QVERIFY(producer.waitForFinished(5000));
#endif
}
void tst_QSharedMemory::uniqueKey_data()
{
QTest::addColumn<QString>("key1");
QTest::addColumn<QString>("key2");
QTest::newRow("null == null") << QString() << QString();
QTest::newRow("key == key") << QString("key") << QString("key");
QTest::newRow("key1 == key1") << QString("key1") << QString("key1");
QTest::newRow("key != key1") << QString("key") << QString("key1");
QTest::newRow("ke1y != key1") << QString("ke1y") << QString("key1");
QTest::newRow("key1 != key2") << QString("key1") << QString("key2");
QTest::newRow("Noël -> Nol") << QString::fromUtf8("N\xc3\xabl") << QString("Nol");
}
void tst_QSharedMemory::uniqueKey()
{
QFETCH(QString, key1);
QFETCH(QString, key2);
QSharedMemory sm1(key1);
QSharedMemory sm2(key2);
bool setEqual = (key1 == key2);
bool keyEqual = (sm1.key() == sm2.key());
bool nativeEqual = (sm1.nativeKey() == sm2.nativeKey());
QCOMPARE(keyEqual, setEqual);
QCOMPARE(nativeEqual, setEqual);
}
QTEST_MAIN(tst_QSharedMemory)
#include "tst_qsharedmemory.moc"