Files
OmaPhoto/tests/ProjectSaveQueueTests.cpp
T

240 lines
9.8 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "SaveHooks.h"
// Saves queued behind a write: one writer at a time.
class ProjectSaveQueueTests : public QObject {
Q_OBJECT
private slots:
void init();
void cleanup();
void threeSavesWriteOneAfterAnother();
void aSaveAskedAsTheToolsFreeWaitsItsTurn();
void aQuitWaitsForTheWriteBeforeAsking();
void aQueuedSaveWithoutADocumentSavesNothing();
void aFailedWriteHoldsItsWaitersUntilAnswered();
void reopeningWhileSavingReadsTheSavedPackage();
void closingATabWaitsForItsWrite();
void aControllerGoneMidWriteLeavesNoCallback();
void anExportDuringAWriteSucceedsBeside();
};
void ProjectSaveQueueTests::init()
{
resetHooks();
}
void ProjectSaveQueueTests::cleanup()
{
resetHooks();
}
void ProjectSaveQueueTests::threeSavesWriteOneAfterAnother()
{
QTemporaryDir root;
const QString path = savedProject(root);
EditorSession session;
const auto controller = opened(session, path);
const QUuid base = session.document().value().layers[0].id;
QStringList order;
for (const QString &name : {QStringLiteral("A"), QStringLiteral("B"), QStringLiteral("C")}) {
session.renameLayer(base, name);
controller->save(false, [&, name](bool saved) {
order << QStringLiteral("%1=%2 disk=%3").arg(name).arg(saved).arg(ProjectStore::load(path).manifest.layers[0].name);
});
}
session.renameLayer(base, QStringLiteral("Last"));
QTRY_COMPARE_WITH_TIMEOUT(order.size(), 3, 10000);
// A captured itself; the rest waited and captured the latest.
QCOMPARE(order, (QStringList{"A=1 disk=A", "B=1 disk=Last", "C=1 disk=Last"}));
QCOMPARE(mostAtOnce.load(), 1);
QVERIFY(!session.isModified());
QVERIFY(!controller->externalChanges.saving);
}
void ProjectSaveQueueTests::aSaveAskedAsTheToolsFreeWaitsItsTurn()
{
QTemporaryDir root;
const QString path = savedProject(root);
EditorSession session;
const auto controller = opened(session, path);
session.renameLayer(session.document().value().layers[0].id, QStringLiteral("First"));
// The moment the tools go free, an observer saves again.
std::optional<bool> first, second;
bool asked = false, wasBusy = false, reserved = false;
QObject::connect(&session, &EditorSession::changed, [&] {
const bool freed = wasBusy && !session.isProjectBusy();
wasBusy = session.isProjectBusy();
if (asked || !freed)
return;
asked = true;
reserved = controller->externalChanges.saving;
controller->save(false, [&](bool saved) { second = saved; });
});
controller->save(false, [&](bool saved) { first = saved; });
QTRY_VERIFY_WITH_TIMEOUT(first.has_value() && second.has_value(), 10000);
QVERIFY(asked);
QVERIFY(reserved);
QVERIFY(first.value() && second.value());
QCOMPARE(mostAtOnce.load(), 1);
}
void ProjectSaveQueueTests::aQuitWaitsForTheWriteBeforeAsking()
{
QTemporaryDir root;
const QString path = savedProject(root);
EditorSession session;
const auto controller = opened(session, path);
QWidget window;
window.show();
controller->window = &window;
const QUuid base = session.document().value().layers[0].id;
QStringList order;
session.renameLayer(base, QStringLiteral("One"));
controller->save(false, [&](bool saved) { order << QStringLiteral("one=%1").arg(saved); });
session.renameLayer(base, QStringLiteral("Two"));
// The quit asks only once the write is whole.
DialogDesk desk;
desk.replies = {"Don’t Save"};
desk.note = [&] { return QStringLiteral("disk=%1 saving=%2").arg(ProjectStore::load(path).manifest.layers[0].name).arg(controller->externalChanges.saving); };
controller->confirmQuit([&](bool quits) { order << QStringLiteral("quit=%1").arg(quits); });
QTRY_COMPARE_WITH_TIMEOUT(order.size(), 2, 10000);
QCOMPARE(order, (QStringList{"one=1", "quit=1"}));
QCOMPARE(desk.seen.size(), 1);
QVERIFY2(desk.seen[0].endsWith("|disk=One saving=0"), qPrintable(desk.seen[0]));
}
void ProjectSaveQueueTests::aQueuedSaveWithoutADocumentSavesNothing()
{
QTemporaryDir root;
EditorSession session;
ProjectController controller(session);
session.createDocument(8, 6);
session.addBlankLayer();
const QString path = root.filePath(QStringLiteral("New.comp"));
DialogDesk desk;
desk.replies = {path};
std::optional<bool> first, second;
controller.save(false, [&](bool saved) { first = saved; });
QTRY_VERIFY_WITH_TIMEOUT(controller.externalChanges.saving, 4000);
controller.save(false, [&](bool saved) { second = saved; });
// Undo takes the canvas back while the first write runs.
while (session.document() && session.canUndo())
session.undo();
QVERIFY(!session.document());
QTRY_VERIFY_WITH_TIMEOUT(first.has_value() && second.has_value(), 10000);
QVERIFY(first.value());
QVERIFY(!second.value());
QVERIFY(!session.isProjectBusy());
QCOMPARE(desk.seen.size(), 1);
}
void ProjectSaveQueueTests::aFailedWriteHoldsItsWaitersUntilAnswered()
{
QTemporaryDir root;
const QString path = savedProject(root);
EditorSession session;
const auto controller = opened(session, path);
const QString locked = root.filePath(QStringLiteral("Locked"));
QVERIFY(QDir().mkpath(locked));
QVERIFY(QFile::setPermissions(locked, QFileDevice::ReadOwner | QFileDevice::ExeOwner));
session.renameLayer(session.document().value().layers[0].id, QStringLiteral("Unsaved"));
std::optional<bool> failed, again, quit;
DialogDesk desk;
desk.replies = {locked + QStringLiteral("/Copy.comp"), "OK", "Don’t Save"};
// With the failure's alert up, a save and quit queue.
desk.note = [&] {
if (desk.seen.size() != 1)
return QString();
controller->save(false, [&](bool saved) { again = saved; });
controller->confirmQuit([&](bool quits) { quit = quits; });
QTest::qWait(300);
return QStringLiteral("again=%1 quit=%2").arg(again.has_value()).arg(quit.has_value());
};
controller->save(true, [&](bool saved) { failed = saved; });
QTRY_VERIFY_WITH_TIMEOUT(failed.has_value() && again.has_value() && quit.has_value(), 10000);
QVERIFY2(desk.seen.value(1).endsWith("|again=0 quit=0"), qPrintable(desk.seen.value(1)));
QVERIFY(!failed.value());
// The quit took the tools first: the save is refused.
QVERIFY(!again.value());
QVERIFY(quit.value());
QCOMPARE(desk.seen.size(), 3);
QCOMPARE(ProjectStore::load(path).manifest.layers[0].name, QString("Base"));
QFile::setPermissions(locked, QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner);
}
void ProjectSaveQueueTests::reopeningWhileSavingReadsTheSavedPackage()
{
QTemporaryDir root;
const QString path = savedProject(root);
EditorSession session;
const auto controller = opened(session, path);
session.renameLayer(session.document().value().layers[0].id, QStringLiteral("Newest saved version"));
std::optional<bool> saved, reopened;
controller->save(false, [&](bool value) { saved = value; });
controller->open(path, [&](bool value) { reopened = value; });
QTRY_VERIFY_WITH_TIMEOUT(saved.has_value() && reopened.has_value(), 10000);
QVERIFY(saved.value() && reopened.value());
QCOMPARE(firstName(session), QString("Newest saved version"));
QVERIFY(!session.isModified());
}
void ProjectSaveQueueTests::closingATabWaitsForItsWrite()
{
QTemporaryDir root;
const QString path = savedProject(root);
ProjectWorkspace workspace;
QVERIFY(answered([&](auto done) { workspace.open(path, done); }));
const std::shared_ptr<ProjectTab> tab = workspace.tabs().back();
tab->session.renameLayer(tab->session.document().value().layers[0].id, QStringLiteral("Written"));
std::optional<bool> saved;
tab->controller.save(false, [&](bool value) { saved = value; });
QVERIFY(tab->controller.externalChanges.saving);
bool closed = false;
workspace.close(tab->id, [&] { closed = true; });
// The tab stays until its package is whole.
QTRY_VERIFY_WITH_TIMEOUT(closed, 10000);
QVERIFY(saved.value());
QVERIFY(std::none_of(workspace.tabs().begin(), workspace.tabs().end(), [&](const auto &each) { return each->id == tab->id; }));
QCOMPARE(ProjectStore::load(path).manifest.layers[0].name, QString("Written"));
}
void ProjectSaveQueueTests::aControllerGoneMidWriteLeavesNoCallback()
{
QTemporaryDir root;
const QString path = savedProject(root);
EditorSession session;
auto controller = opened(session, path);
session.renameLayer(session.document().value().layers[0].id, QStringLiteral("Written"));
bool first = false, second = false;
controller->save(false, [&](bool) { first = true; });
controller->save(false, [&](bool) { second = true; });
controller.reset();
QTest::qWait(1500);
QVERIFY(!first && !second);
// The worker finished its package all the same.
QCOMPARE(ProjectStore::load(path).manifest.layers[0].name, QString("Written"));
}
void ProjectSaveQueueTests::anExportDuringAWriteSucceedsBeside()
{
QTemporaryDir root;
const QString path = savedProject(root);
EditorSession session;
const auto controller = opened(session, path);
session.renameLayer(session.document().value().layers[0].id, QStringLiteral("Written"));
std::optional<bool> saved;
controller->save(false, [&](bool value) { saved = value; });
// The tools are free: an export starts beside the write.
const QString png = root.filePath(QStringLiteral("Picture.png"));
DialogDesk desk;
desk.replies = {png};
bool exported = false;
controller->exportPNG([&] { exported = true; });
QTRY_VERIFY_WITH_TIMEOUT(exported && saved.has_value(), 10000);
QVERIFY(saved.value());
QVERIFY(QFile::exists(png));
QCOMPARE(ProjectStore::load(path).manifest.layers[0].name, QString("Written"));
}
QTEST_MAIN(ProjectSaveQueueTests)
#include "ProjectSaveQueueTests.moc"