900 lines
27 KiB
C++

/*
Gwenview: an image viewer
Copyright 2007 Aurélien Gâteau <agateau@kde.org>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// Qt
#include <QConicalGradient>
#include <QImage>
#include <QPainter>
#include <QTest>
// KF
#include <KIO/StatJob>
#include <KJobUiDelegate>
#include <kio_version.h>
// KDCraw
#include <KDCRAW/KDcraw>
// Local
#include "../lib/abstractimageoperation.h"
#include "../lib/document/abstractdocumenteditor.h"
#include "../lib/document/documentfactory.h"
#include "../lib/document/documentjob.h"
#include "../lib/imagemetainfomodel.h"
#include "../lib/imageutils.h"
#include "../lib/transformimageoperation.h"
#include "testutils.h"
#include <exiv2/exif.hpp>
#include "documenttest.h"
QTEST_MAIN(DocumentTest)
using namespace Gwenview;
static void waitUntilMetaInfoLoaded(Document::Ptr doc)
{
while (doc->loadingState() < Document::MetaInfoLoaded) {
QTest::qWait(100);
}
}
static bool waitUntilJobIsDone(DocumentJob *job)
{
JobWatcher watcher(job);
watcher.wait();
return watcher.error() == KJob::NoError;
}
void DocumentTest::initTestCase()
{
qRegisterMetaType<QUrl>("QUrl");
}
void DocumentTest::init()
{
DocumentFactory::instance()->clearCache();
}
void DocumentTest::testLoad()
{
QFETCH(QString, fileName);
QFETCH(QByteArray, expectedFormat);
QFETCH(int, expectedKindInt);
QFETCH(bool, expectedIsAnimated);
QFETCH(QImage, expectedImage);
QFETCH(int, maxHeight); // number of lines to test. -1 to test all lines
auto expectedKind = MimeTypeUtils::Kind(expectedKindInt);
QUrl url = urlForTestFile(fileName);
// testing RAW loading. For raw, QImage directly won't work -> load it using KDCRaw
QByteArray mFormatHint = url.fileName().section('.', -1).toLocal8Bit().toLower();
if (KDcrawIface::KDcraw::rawFilesList().contains(QString(mFormatHint))) {
if (!KDcrawIface::KDcraw::loadEmbeddedPreview(expectedImage, url.toLocalFile())) {
QSKIP(
"Not running this test: failed to get expectedImage. Try running ./fetch_testing_raw.sh\
in the tests/data directory and then rerun the tests.");
}
}
if (expectedKind != MimeTypeUtils::KIND_SVG_IMAGE) {
if (expectedImage.isNull()) {
QSKIP("Not running this test: QImage failed to load the test image");
}
}
Document::Ptr doc = DocumentFactory::instance()->load(url);
QSignalSpy spy(doc.data(), SIGNAL(isAnimatedUpdated()));
doc->waitUntilLoaded();
QCOMPARE(doc->loadingState(), Document::Loaded);
QCOMPARE(doc->kind(), expectedKind);
QCOMPARE(doc->isAnimated(), expectedIsAnimated);
QCOMPARE(spy.count(), doc->isAnimated() ? 1 : 0);
if (doc->kind() == MimeTypeUtils::KIND_RASTER_IMAGE) {
QImage image = doc->image();
if (maxHeight > -1) {
QRect poiRect(0, 0, image.width(), maxHeight);
image = image.copy(poiRect);
expectedImage = expectedImage.copy(poiRect);
}
QCOMPARE(image, expectedImage);
QCOMPARE(QString(doc->format()), QString(expectedFormat));
}
}
static void testLoad_newRow(const char *fileName,
const QByteArray &format,
MimeTypeUtils::Kind kind = MimeTypeUtils::KIND_RASTER_IMAGE,
bool isAnimated = false,
int maxHeight = -1)
{
QTest::newRow(fileName) << fileName << QByteArray(format) << int(kind) << isAnimated << QImage(pathForTestFile(fileName), format) << maxHeight;
}
void DocumentTest::testLoad_data()
{
QTest::addColumn<QString>("fileName");
QTest::addColumn<QByteArray>("expectedFormat");
QTest::addColumn<int>("expectedKindInt");
QTest::addColumn<bool>("expectedIsAnimated");
QTest::addColumn<QImage>("expectedImage");
QTest::addColumn<int>("maxHeight");
testLoad_newRow("test.png", "png");
testLoad_newRow("160216_no_size_before_decoding.eps", "eps");
testLoad_newRow("160382_corrupted.jpeg", "jpeg", MimeTypeUtils::KIND_RASTER_IMAGE, false, 55);
testLoad_newRow("1x10k.png", "png");
testLoad_newRow("1x10k.jpg", "jpeg");
testLoad_newRow("test.xcf", "xcf");
testLoad_newRow("188191_does_not_load.tga", "tga");
testLoad_newRow("289819_does_not_load.png", "png");
testLoad_newRow("png-with-jpeg-extension.jpg", "png");
testLoad_newRow("jpg-with-gif-extension.gif", "jpeg");
// RAW preview
testLoad_newRow("CANON-EOS350D-02.CR2", "cr2", MimeTypeUtils::KIND_RASTER_IMAGE, false);
testLoad_newRow("dsc_0093.nef", "nef", MimeTypeUtils::KIND_RASTER_IMAGE, false);
// SVG
testLoad_newRow("test.svg", "", MimeTypeUtils::KIND_SVG_IMAGE);
// FIXME: Test svgz
// Animated
testLoad_newRow("4frames.gif", "gif", MimeTypeUtils::KIND_RASTER_IMAGE, true);
testLoad_newRow("1frame.gif", "gif", MimeTypeUtils::KIND_RASTER_IMAGE, false);
testLoad_newRow("185523_1frame_with_graphic_control_extension.gif", "gif", MimeTypeUtils::KIND_RASTER_IMAGE, false);
}
void DocumentTest::testLoadTwoPasses()
{
QUrl url = urlForTestFile("test.png");
QImage image;
bool ok = image.load(url.toLocalFile());
QVERIFY2(ok, "Could not load 'test.png'");
Document::Ptr doc = DocumentFactory::instance()->load(url);
waitUntilMetaInfoLoaded(doc);
QVERIFY2(doc->image().isNull(), "Image shouldn't have been loaded at this time");
QCOMPARE(doc->format().data(), "png");
doc->waitUntilLoaded();
QCOMPARE(image, doc->image());
}
void DocumentTest::testLoadEmpty()
{
QUrl url = urlForTestFile("empty.png");
Document::Ptr doc = DocumentFactory::instance()->load(url);
while (doc->loadingState() <= Document::KindDetermined) {
QTest::qWait(100);
}
QCOMPARE(doc->loadingState(), Document::LoadingFailed);
}
#define NEW_ROW(fileName) QTest::newRow(fileName) << fileName
void DocumentTest::testLoadDownSampled_data()
{
QTest::addColumn<QString>("fileName");
NEW_ROW("orient6.jpg");
NEW_ROW("1x10k.jpg");
}
#undef NEW_ROW
void DocumentTest::testLoadDownSampled()
{
// Note: for now we only support down sampling on jpeg, do not use test.png
// here
QFETCH(QString, fileName);
QUrl url = urlForTestFile(fileName);
QImage image;
bool ok = image.load(url.toLocalFile());
QVERIFY2(ok, "Could not load test image");
Document::Ptr doc = DocumentFactory::instance()->load(url);
QSignalSpy downSampledImageReadySpy(doc.data(), SIGNAL(downSampledImageReady()));
QSignalSpy loadingFailedSpy(doc.data(), SIGNAL(loadingFailed(QUrl)));
QSignalSpy loadedSpy(doc.data(), SIGNAL(loaded(QUrl)));
bool ready = doc->prepareDownSampledImageForZoom(0.2);
QVERIFY2(!ready, "There should not be a down sampled image at this point");
while (downSampledImageReadySpy.isEmpty() && loadingFailedSpy.isEmpty() && loadedSpy.isEmpty()) {
QTest::qWait(100);
}
QImage downSampledImage = doc->downSampledImageForZoom(0.2);
QVERIFY2(!downSampledImage.isNull(), "Down sampled image should not be null");
QSize expectedSize = doc->size() / 2;
if (expectedSize.isEmpty()) {
expectedSize = image.size();
}
QCOMPARE(downSampledImage.size(), expectedSize);
}
/**
* Down sampling is not supported on png. We should get a complete image
* instead.
*/
void DocumentTest::testLoadDownSampledPng()
{
QUrl url = urlForTestFile("test.png");
QImage image;
bool ok = image.load(url.toLocalFile());
QVERIFY2(ok, "Could not load test image");
Document::Ptr doc = DocumentFactory::instance()->load(url);
LoadingStateSpy stateSpy(doc);
connect(doc.data(), &Document::loaded, &stateSpy, &LoadingStateSpy::readState);
bool ready = doc->prepareDownSampledImageForZoom(0.2);
QVERIFY2(!ready, "There should not be a down sampled image at this point");
doc->waitUntilLoaded();
QCOMPARE(stateSpy.mCallCount, 1);
QCOMPARE(stateSpy.mState, Document::Loaded);
}
void DocumentTest::testLoadRemote()
{
QUrl url = setUpRemoteTestDir("test.png");
if (!url.isValid()) {
QSKIP("Not running this test: failed to setup remote test dir.");
}
url = url.adjusted(QUrl::StripTrailingSlash);
url.setPath(url.path() + '/' + "test.png");
QVERIFY2(KIO::stat(url, KIO::StatJob::SourceSide, KIO::StatNoDetails)->exec(), "test url not found");
Document::Ptr doc = DocumentFactory::instance()->load(url);
doc->waitUntilLoaded();
QImage image = doc->image();
QCOMPARE(image.width(), 150);
QCOMPARE(image.height(), 100);
}
void DocumentTest::testLoadAnimated()
{
QUrl srcUrl = urlForTestFile("40frames.gif");
Document::Ptr doc = DocumentFactory::instance()->load(srcUrl);
QSignalSpy spy(doc.data(), SIGNAL(imageRectUpdated(QRect)));
doc->waitUntilLoaded();
QVERIFY(doc->isAnimated());
// Test we receive only one imageRectUpdated() until animation is started
// (the imageRectUpdated() is triggered by the loading of the first image)
QTest::qWait(1000);
QCOMPARE(spy.count(), 1);
// Test we now receive some imageRectUpdated()
doc->startAnimation();
QTest::qWait(1000);
int count = spy.count();
doc->stopAnimation();
QVERIFY2(count > 0, "No imageRectUpdated() signal received");
// Test we do not receive imageRectUpdated() anymore
QTest::qWait(1000);
QCOMPARE(count, spy.count());
// Start again, we should receive imageRectUpdated() again
doc->startAnimation();
QTest::qWait(1000);
QVERIFY2(spy.count() > count, "No imageRectUpdated() signal received after restarting");
}
void DocumentTest::testPrepareDownSampledAfterFailure()
{
QUrl url = urlForTestFile("empty.png");
Document::Ptr doc = DocumentFactory::instance()->load(url);
doc->waitUntilLoaded();
QCOMPARE(doc->loadingState(), Document::LoadingFailed);
bool ready = doc->prepareDownSampledImageForZoom(0.25);
QVERIFY2(!ready, "Down sampled image should not be ready");
}
void DocumentTest::testSaveRemote()
{
QUrl dstUrl = setUpRemoteTestDir();
if (!dstUrl.isValid()) {
QSKIP("Not running this test: failed to setup remote test dir.");
}
QUrl srcUrl = urlForTestFile("test.png");
Document::Ptr doc = DocumentFactory::instance()->load(srcUrl);
doc->waitUntilLoaded();
dstUrl = dstUrl.adjusted(QUrl::StripTrailingSlash);
dstUrl.setPath(dstUrl.path() + '/' + "testSaveRemote.png");
QVERIFY(waitUntilJobIsDone(doc->save(dstUrl, "png")));
}
/**
* Check that deleting a document while it is loading does not crash
*/
void DocumentTest::testDeleteWhileLoading()
{
{
QUrl url = urlForTestFile("test.png");
QImage image;
bool ok = image.load(url.toLocalFile());
QVERIFY2(ok, "Could not load 'test.png'");
Document::Ptr doc = DocumentFactory::instance()->load(url);
}
DocumentFactory::instance()->clearCache();
// Wait two seconds. If the test fails we will get a segfault while waiting
QTest::qWait(2000);
}
void DocumentTest::testLoadRotated()
{
QUrl url = urlForTestFile("orient6.jpg");
QImage image;
bool ok = image.load(url.toLocalFile());
QVERIFY2(ok, "Could not load 'orient6.jpg'");
QTransform matrix = ImageUtils::transformMatrix(ROT_90);
image = image.transformed(matrix);
Document::Ptr doc = DocumentFactory::instance()->load(url);
doc->waitUntilLoaded();
QCOMPARE(image, doc->image());
// RAW preview on rotated image
url = urlForTestFile("dsd_1838.nef");
if (!KDcrawIface::KDcraw::loadEmbeddedPreview(image, url.toLocalFile())) {
QSKIP(
"Not running this test: failed to get image. Try running ./fetch_testing_raw.sh\
in the tests/data directory and then rerun the tests.");
}
matrix = ImageUtils::transformMatrix(ROT_270);
image = image.transformed(matrix);
doc = DocumentFactory::instance()->load(url);
doc->waitUntilLoaded();
QCOMPARE(image, doc->image());
}
/**
* Checks that asking the DocumentFactory the same document twice in a row does
* not load it twice
*/
void DocumentTest::testMultipleLoads()
{
QUrl url = urlForTestFile("orient6.jpg");
Document::Ptr doc1 = DocumentFactory::instance()->load(url);
Document::Ptr doc2 = DocumentFactory::instance()->load(url);
QCOMPARE(doc1.data(), doc2.data());
}
void DocumentTest::testSaveAs()
{
QUrl url = urlForTestFile("orient6.jpg");
DocumentFactory *factory = DocumentFactory::instance();
Document::Ptr doc = factory->load(url);
QSignalSpy savedSpy(doc.data(), SIGNAL(saved(QUrl, QUrl)));
QSignalSpy modifiedDocumentListChangedSpy(factory, SIGNAL(modifiedDocumentListChanged()));
QSignalSpy documentChangedSpy(factory, SIGNAL(documentChanged(QUrl)));
doc->startLoadingFullImage();
QUrl destUrl = urlForTestOutputFile("result.png");
QVERIFY(waitUntilJobIsDone(doc->save(destUrl, "png")));
QCOMPARE(doc->format().data(), "png");
QCOMPARE(doc->url(), destUrl);
QCOMPARE(doc->metaInfo()->getValueForKey("General.Name"), destUrl.fileName());
QVERIFY2(doc->loadingState() == Document::Loaded, "Document is supposed to finish loading before saving");
QTest::qWait(100); // saved() is emitted asynchronously
QCOMPARE(savedSpy.count(), 1);
QVariantList args = savedSpy.takeFirst();
QCOMPARE(args.at(0).toUrl(), url);
QCOMPARE(args.at(1).toUrl(), destUrl);
QImage image("result.png", "png");
QCOMPARE(doc->image(), image);
QVERIFY(!DocumentFactory::instance()->hasUrl(url));
QVERIFY(DocumentFactory::instance()->hasUrl(destUrl));
QCOMPARE(modifiedDocumentListChangedSpy.count(), 0); // No changes were made
QCOMPARE(documentChangedSpy.count(), 1);
args = documentChangedSpy.takeFirst();
QCOMPARE(args.at(0).toUrl(), destUrl);
}
void DocumentTest::testLosslessSave()
{
QUrl url1 = urlForTestFile("orient6.jpg");
Document::Ptr doc = DocumentFactory::instance()->load(url1);
doc->startLoadingFullImage();
QUrl url2 = urlForTestOutputFile("orient1.jpg");
QVERIFY(waitUntilJobIsDone(doc->save(url2, "jpeg")));
QImage image1;
QVERIFY(image1.load(url1.toLocalFile()));
QImage image2;
QVERIFY(image2.load(url2.toLocalFile()));
QCOMPARE(image1, image2);
}
void DocumentTest::testLosslessRotate()
{
// Generate test image
QImage image1(200, 96, QImage::Format_RGB32);
{
QPainter painter(&image1);
QConicalGradient gradient(QPointF(100, 48), 100);
gradient.setColorAt(0, Qt::white);
gradient.setColorAt(1, Qt::blue);
painter.fillRect(image1.rect(), gradient);
}
QUrl url1 = urlForTestOutputFile("lossless1.jpg");
QVERIFY(image1.save(url1.toLocalFile(), "jpeg"));
// Load it as a Gwenview document
Document::Ptr doc = DocumentFactory::instance()->load(url1);
doc->waitUntilLoaded();
// Rotate one time
QVERIFY(doc->editor());
doc->editor()->applyTransformation(ROT_90);
// Save it
QUrl url2 = urlForTestOutputFile("lossless2.jpg");
waitUntilJobIsDone(doc->save(url2, "jpeg"));
// Load the saved image
doc = DocumentFactory::instance()->load(url2);
doc->waitUntilLoaded();
// Rotate the other way
QVERIFY(doc->editor());
doc->editor()->applyTransformation(ROT_270);
waitUntilJobIsDone(doc->save(url2, "jpeg"));
// Compare the saved images
QVERIFY(image1.load(url1.toLocalFile()));
QImage image2;
QVERIFY(image2.load(url2.toLocalFile()));
QCOMPARE(image1, image2);
}
void DocumentTest::testModifyAndSaveAs()
{
QVariantList args;
class TestOperation : public AbstractImageOperation
{
public:
void redo() override
{
QImage image(10, 10, QImage::Format_ARGB32);
image.fill(QColor(Qt::white).rgb());
document()->editor()->setImage(image);
finish(true);
}
};
QUrl url = urlForTestFile("orient6.jpg");
DocumentFactory *factory = DocumentFactory::instance();
Document::Ptr doc = factory->load(url);
QSignalSpy savedSpy(doc.data(), SIGNAL(saved(QUrl, QUrl)));
QSignalSpy modifiedDocumentListChangedSpy(factory, SIGNAL(modifiedDocumentListChanged()));
QSignalSpy documentChangedSpy(factory, SIGNAL(documentChanged(QUrl)));
doc->waitUntilLoaded();
QVERIFY(!doc->isModified());
QCOMPARE(modifiedDocumentListChangedSpy.count(), 0);
// Modify image
QVERIFY(doc->editor());
auto op = new TestOperation;
op->applyToDocument(doc);
QTest::qWait(100);
QVERIFY(doc->isModified());
QCOMPARE(modifiedDocumentListChangedSpy.count(), 1);
modifiedDocumentListChangedSpy.clear();
QList<QUrl> lst = factory->modifiedDocumentList();
QCOMPARE(lst.count(), 1);
QCOMPARE(lst.first(), url);
QCOMPARE(documentChangedSpy.count(), 1);
args = documentChangedSpy.takeFirst();
QCOMPARE(args.at(0).toUrl(), url);
// Save it under a new name
QUrl destUrl = urlForTestOutputFile("modify.png");
QVERIFY(waitUntilJobIsDone(doc->save(destUrl, "png")));
// Wait a bit because save() will clear the undo stack when back to the
// event loop
QTest::qWait(100);
QVERIFY(!doc->isModified());
QVERIFY(!factory->hasUrl(url));
QVERIFY(factory->hasUrl(destUrl));
QCOMPARE(modifiedDocumentListChangedSpy.count(), 1);
QVERIFY(DocumentFactory::instance()->modifiedDocumentList().isEmpty());
QCOMPARE(documentChangedSpy.count(), 2);
QList<QUrl> modifiedUrls = QList<QUrl>() << url << destUrl;
QVERIFY(modifiedUrls.contains(url));
QVERIFY(modifiedUrls.contains(destUrl));
}
void DocumentTest::testMetaInfoJpeg()
{
QUrl url = urlForTestFile("orient6.jpg");
Document::Ptr doc = DocumentFactory::instance()->load(url);
// We cleared the cache, so the document should not be loaded
Q_ASSERT(doc->loadingState() <= Document::KindDetermined);
// Wait until we receive the metaInfoUpdated() signal
QSignalSpy metaInfoUpdatedSpy(doc.data(), SIGNAL(metaInfoUpdated()));
while (metaInfoUpdatedSpy.isEmpty()) {
QTest::qWait(100);
}
// Extract an exif key
QString value = doc->metaInfo()->getValueForKey("Exif.Image.Make");
QCOMPARE(value, QString::fromUtf8("Canon"));
}
void DocumentTest::testMetaInfoBmp()
{
QUrl url = urlForTestOutputFile("metadata.bmp");
const int width = 200;
const int height = 100;
QImage image(width, height, QImage::Format_ARGB32);
image.fill(Qt::black);
image.save(url.toLocalFile(), "BMP");
Document::Ptr doc = DocumentFactory::instance()->load(url);
QSignalSpy metaInfoUpdatedSpy(doc.data(), SIGNAL(metaInfoUpdated()));
waitUntilMetaInfoLoaded(doc);
Q_ASSERT(metaInfoUpdatedSpy.count() >= 1);
QString value = doc->metaInfo()->getValueForKey("General.ImageSize");
QString expectedValue = QStringLiteral("%1x%2").arg(width).arg(height);
QCOMPARE(value, expectedValue);
}
void DocumentTest::testForgetModifiedDocument()
{
QSignalSpy spy(DocumentFactory::instance(), SIGNAL(modifiedDocumentListChanged()));
DocumentFactory::instance()->forget(QUrl("file://does/not/exist.png"));
QCOMPARE(spy.count(), 0);
// Generate test image
QImage image1(200, 96, QImage::Format_RGB32);
{
QPainter painter(&image1);
QConicalGradient gradient(QPointF(100, 48), 100);
gradient.setColorAt(0, Qt::white);
gradient.setColorAt(1, Qt::blue);
painter.fillRect(image1.rect(), gradient);
}
QUrl url = urlForTestOutputFile("testForgetModifiedDocument.png");
QVERIFY(image1.save(url.toLocalFile(), "png"));
// Load it as a Gwenview document
Document::Ptr doc = DocumentFactory::instance()->load(url);
doc->waitUntilLoaded();
// Modify it
auto op = new TransformImageOperation(ROT_90);
op->applyToDocument(doc);
QTest::qWait(100);
QCOMPARE(spy.count(), 1);
QList<QUrl> lst = DocumentFactory::instance()->modifiedDocumentList();
QCOMPARE(lst.length(), 1);
QCOMPARE(lst.first(), url);
// Forget it
DocumentFactory::instance()->forget(url);
QCOMPARE(spy.count(), 2);
lst = DocumentFactory::instance()->modifiedDocumentList();
QVERIFY(lst.isEmpty());
}
void DocumentTest::testModifiedAndSavedSignals()
{
TransformImageOperation *op;
QUrl url = urlForTestFile("orient6.jpg");
Document::Ptr doc = DocumentFactory::instance()->load(url);
QSignalSpy modifiedSpy(doc.data(), SIGNAL(modified(QUrl)));
QSignalSpy savedSpy(doc.data(), SIGNAL(saved(QUrl, QUrl)));
doc->waitUntilLoaded();
QCOMPARE(modifiedSpy.count(), 0);
QCOMPARE(savedSpy.count(), 0);
op = new TransformImageOperation(ROT_90);
op->applyToDocument(doc);
QTest::qWait(100);
QCOMPARE(modifiedSpy.count(), 1);
op = new TransformImageOperation(ROT_90);
op->applyToDocument(doc);
QTest::qWait(100);
QCOMPARE(modifiedSpy.count(), 2);
doc->undoStack()->undo();
QTest::qWait(100);
QCOMPARE(modifiedSpy.count(), 3);
doc->undoStack()->undo();
QTest::qWait(100);
QCOMPARE(savedSpy.count(), 1);
}
class TestJob : public DocumentJob
{
public:
TestJob(QString *str, char ch)
: mStr(str)
, mCh(ch)
{
}
protected:
void doStart() override
{
*mStr += mCh;
emitResult();
}
private:
QString *mStr;
char mCh;
};
void DocumentTest::testJobQueue()
{
QUrl url = urlForTestFile("orient6.jpg");
Document::Ptr doc = DocumentFactory::instance()->load(url);
QSignalSpy spy(doc.data(), SIGNAL(busyChanged(QUrl, bool)));
QString str;
doc->enqueueJob(new TestJob(&str, 'a'));
doc->enqueueJob(new TestJob(&str, 'b'));
doc->enqueueJob(new TestJob(&str, 'c'));
QVERIFY(doc->isBusy());
QEventLoop loop;
connect(doc.data(), &Document::allTasksDone, &loop, &QEventLoop::quit);
loop.exec();
QVERIFY(!doc->isBusy());
QCOMPARE(spy.count(), 2);
QVariantList row = spy.takeFirst();
QCOMPARE(row.at(0).toUrl(), url);
QVERIFY(row.at(1).toBool());
row = spy.takeFirst();
QCOMPARE(row.at(0).toUrl(), url);
QVERIFY(!row.at(1).toBool());
QCOMPARE(str, QStringLiteral("abc"));
}
class TestCheckDocumentEditorJob : public DocumentJob
{
public:
TestCheckDocumentEditorJob(int *hasEditor)
: mHasEditor(hasEditor)
{
*mHasEditor = -1;
}
protected:
void doStart() override
{
document()->waitUntilLoaded();
*mHasEditor = checkDocumentEditor() ? 1 : 0;
emitResult();
}
private:
int *mHasEditor;
};
class TestUiDelegate : public KJobUiDelegate
{
public:
TestUiDelegate(bool *showErrorMessageCalled)
: mShowErrorMessageCalled(showErrorMessageCalled)
{
setAutoErrorHandlingEnabled(true);
*mShowErrorMessageCalled = false;
}
void showErrorMessage() override
{
// qDebug();
*mShowErrorMessageCalled = true;
}
private:
bool *mShowErrorMessageCalled;
};
/**
* Test that an error is reported when a DocumentJob fails because there is no
* document editor available
*/
void DocumentTest::testCheckDocumentEditor()
{
int hasEditor;
bool showErrorMessageCalled;
QEventLoop loop;
Document::Ptr doc;
TestCheckDocumentEditorJob *job;
doc = DocumentFactory::instance()->load(urlForTestFile("orient6.jpg"));
job = new TestCheckDocumentEditorJob(&hasEditor);
job->setUiDelegate(new TestUiDelegate(&showErrorMessageCalled));
doc->enqueueJob(job);
connect(doc.data(), &Document::allTasksDone, &loop, &QEventLoop::quit);
loop.exec();
QVERIFY(!showErrorMessageCalled);
QCOMPARE(hasEditor, 1);
doc = DocumentFactory::instance()->load(urlForTestFile("test.svg"));
job = new TestCheckDocumentEditorJob(&hasEditor);
job->setUiDelegate(new TestUiDelegate(&showErrorMessageCalled));
doc->enqueueJob(job);
connect(doc.data(), &Document::allTasksDone, &loop, &QEventLoop::quit);
loop.exec();
QVERIFY(showErrorMessageCalled);
QCOMPARE(hasEditor, 0);
}
/**
* An operation should only pushed to the document undo stack if it succeed
*/
void DocumentTest::testUndoStackPush()
{
class SuccessOperation : public AbstractImageOperation
{
protected:
void redo() override
{
QMetaObject::invokeMethod(
this,
[this]() {
finish(true);
},
Qt::QueuedConnection);
}
};
class FailureOperation : public AbstractImageOperation
{
protected:
void redo() override
{
QMetaObject::invokeMethod(
this,
[this]() {
finish(false);
},
Qt::QueuedConnection);
}
};
AbstractImageOperation *op;
Document::Ptr doc = DocumentFactory::instance()->load(urlForTestFile("orient6.jpg"));
// A successful operation should be added to the undo stack
op = new SuccessOperation;
op->applyToDocument(doc);
QTest::qWait(100);
QVERIFY(!doc->undoStack()->isClean());
// Reset
doc->undoStack()->undo();
QVERIFY(doc->undoStack()->isClean());
// A failed operation should not be added to the undo stack
op = new FailureOperation;
op->applyToDocument(doc);
QTest::qWait(100);
QVERIFY(doc->undoStack()->isClean());
}
void DocumentTest::testUndoRedo()
{
class SuccessOperation : public AbstractImageOperation
{
public:
int mRedoCount = 0;
int mUndoCount = 0;
protected:
void redo() override
{
mRedoCount++;
finish(true);
}
void undo() override
{
mUndoCount++;
finish(true);
}
};
Document::Ptr doc = DocumentFactory::instance()->load(urlForTestFile("orient6.jpg"));
QSignalSpy modifiedSpy(doc.data(), &Document::modified);
QSignalSpy savedSpy(doc.data(), &Document::saved);
auto op = new SuccessOperation;
QCOMPARE(op->mRedoCount, 0);
QCOMPARE(op->mUndoCount, 0);
// Apply (redo) operation
op->applyToDocument(doc);
QVERIFY(modifiedSpy.wait());
QCOMPARE(op->mRedoCount, 1);
QCOMPARE(op->mUndoCount, 0);
QCOMPARE(doc->undoStack()->count(), 1);
QVERIFY(!doc->undoStack()->isClean());
// Undo operation
doc->undoStack()->undo();
QVERIFY(savedSpy.wait());
QCOMPARE(op->mRedoCount, 1);
QCOMPARE(op->mUndoCount, 1);
QCOMPARE(doc->undoStack()->count(), 1);
QVERIFY(doc->undoStack()->isClean());
// Redo operation
doc->undoStack()->redo();
QVERIFY(modifiedSpy.wait());
QCOMPARE(op->mRedoCount, 2);
QCOMPARE(op->mUndoCount, 1);
QCOMPARE(doc->undoStack()->count(), 1);
QVERIFY(!doc->undoStack()->isClean());
// Undo operation again
doc->undoStack()->undo();
QVERIFY(savedSpy.wait());
QCOMPARE(op->mRedoCount, 2);
QCOMPARE(op->mUndoCount, 2);
QCOMPARE(doc->undoStack()->count(), 1);
QVERIFY(doc->undoStack()->isClean());
}
#include "moc_documenttest.cpp"