263 lines
11 KiB
C++
263 lines
11 KiB
C++
#include "ExternalChangeFixtures.h"
|
|
|
|
// What holds a change back, and what a reload keeps.
|
|
class ExternalChangeGateTests : public QObject {
|
|
Q_OBJECT
|
|
private slots:
|
|
void aFailedSaveKeepsItsGuardUntilTheAlertIsAnswered();
|
|
void aManagingWorkspaceHoldsTheCheck();
|
|
void theBackOffDoublesToACap();
|
|
void aLoadLandingElsewhereInstallsNothing();
|
|
void aReloadKeepsTheWholeViewAndAnnouncesItLast();
|
|
void anUnreadableManifestAsksNothing();
|
|
void saveAsAndOpenMoveTheWatch();
|
|
void aSecondChangeWhileAskingIsHeard();
|
|
};
|
|
|
|
void ExternalChangeGateTests::aFailedSaveKeepsItsGuardUntilTheAlertIsAnswered()
|
|
{
|
|
QTemporaryDir root;
|
|
const QString path = savedProject(root);
|
|
EditorSession session;
|
|
const auto controller = opened(session, path);
|
|
QWidget window;
|
|
window.show();
|
|
controller->window = &window;
|
|
session.renameLayer(session.document().value().layers[0].id, QStringLiteral("Unsaved here"));
|
|
// Save As into a folder that takes nothing.
|
|
const QString locked = root.filePath(QStringLiteral("Locked"));
|
|
QVERIFY(QDir().mkpath(locked));
|
|
QVERIFY(QFile::setPermissions(locked, QFileDevice::ReadOwner | QFileDevice::ExeOwner));
|
|
DialogDesk desk;
|
|
desk.replies = {locked + QStringLiteral("/Copy.comp"), "OK"};
|
|
// While the alert is up, changes on disk drop.
|
|
desk.note = [&] {
|
|
renameFirstLayerOnDisk(path, QStringLiteral("Renamed elsewhere"));
|
|
QTest::qWait(900);
|
|
return QStringLiteral("saving=%1 reloads=%2").arg(controller->externalChanges.saving).arg(controller->externalChanges.reloadCount);
|
|
};
|
|
QVERIFY(!answered([&](auto done) { controller->save(true, done); }));
|
|
QCOMPARE(desk.seen.size(), 2);
|
|
QVERIFY2(desk.seen[1].endsWith("|saving=1 reloads=0"), qPrintable(desk.seen[1]));
|
|
QVERIFY(!controller->externalChanges.saving);
|
|
settle();
|
|
QCOMPARE(desk.seen.size(), 2);
|
|
QCOMPARE(controller->externalChanges.reloadCount, 0);
|
|
QCOMPARE(firstName(session), QString("Unsaved here"));
|
|
QFile::setPermissions(locked, QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner);
|
|
}
|
|
|
|
void ExternalChangeGateTests::aManagingWorkspaceHoldsTheCheck()
|
|
{
|
|
QTemporaryDir root;
|
|
const QString path = savedProject(root);
|
|
ProjectWorkspace workspace;
|
|
QWidget window;
|
|
window.show();
|
|
workspace.window = &window;
|
|
QVERIFY(answered([&](auto done) { workspace.open(path, done); }));
|
|
const std::shared_ptr<ProjectTab> watched = workspace.tabs().back();
|
|
workspace.newCanvas();
|
|
const std::shared_ptr<ProjectTab> other = workspace.tabs().back();
|
|
other->session.createDocument(8, 6);
|
|
other->session.addBlankLayer();
|
|
workspace.select(watched->id);
|
|
// Closing the other tab asks; meanwhile the workspace manages.
|
|
DialogDesk desk;
|
|
desk.replies = {"Cancel"};
|
|
desk.note = [&] {
|
|
renameFirstLayerOnDisk(path, QStringLiteral("Renamed elsewhere"));
|
|
QTest::qWait(900);
|
|
return QStringLiteral("managing=%1 reloads=%2 recheck=%3")
|
|
.arg(workspace.isManaging())
|
|
.arg(watched->controller.externalChanges.reloadCount)
|
|
.arg(watched->controller.externalChanges.recheck->isActive());
|
|
};
|
|
std::optional<bool> closed;
|
|
workspace.close(other->id, [&] { closed = true; });
|
|
QTRY_VERIFY(closed.has_value());
|
|
QCOMPARE(desk.seen.size(), 1);
|
|
QVERIFY2(desk.seen[0].endsWith("|managing=1 reloads=0 recheck=1"), qPrintable(desk.seen[0]));
|
|
QTRY_COMPARE_WITH_TIMEOUT(watched->controller.externalChanges.reloadCount, 1, 4000);
|
|
QCOMPARE(firstName(watched->session), QString("Renamed elsewhere"));
|
|
}
|
|
|
|
void ExternalChangeGateTests::theBackOffDoublesToACap()
|
|
{
|
|
QTemporaryDir root;
|
|
const QString path = savedProject(root);
|
|
EditorSession session;
|
|
const auto controller = opened(session, path);
|
|
ExternalChangeState &state = controller->externalChanges;
|
|
QCOMPARE(state.recheck->timerType(), Qt::PreciseTimer);
|
|
session.setIsProjectBusy(true);
|
|
renameFirstLayerOnDisk(path, QStringLiteral("Renamed elsewhere"));
|
|
QTRY_VERIFY_WITH_TIMEOUT(state.recheck->isActive(), 4000);
|
|
QCOMPARE(state.recheck->interval(), 250);
|
|
// Each busy check waits twice as long, to 32 seconds.
|
|
for (const int expected : {500, 1000, 2000, 4000, 8000, 16000, 32000, 32000}) {
|
|
state.pending = true;
|
|
controller->resumeExternalChangeCheck();
|
|
QTRY_COMPARE(state.recheck->interval(), expected);
|
|
QVERIFY(state.recheck->isActive());
|
|
}
|
|
QCOMPARE(state.recheckAttempt, 7);
|
|
session.setIsProjectBusy(false);
|
|
state.pending = true;
|
|
controller->resumeExternalChangeCheck();
|
|
QTRY_COMPARE_WITH_TIMEOUT(state.reloadCount, 1, 4000);
|
|
QCOMPARE(state.recheckAttempt, 0);
|
|
}
|
|
|
|
void ExternalChangeGateTests::aLoadLandingElsewhereInstallsNothing()
|
|
{
|
|
for (const bool clears : {false, true}) {
|
|
QTemporaryDir root;
|
|
const QString path = savedProject(root);
|
|
EditorSession session;
|
|
const auto controller = opened(session, path);
|
|
// As the reload begins, the project moves or closes.
|
|
bool moved = false;
|
|
QObject::connect(&session, &EditorSession::changed, [&] {
|
|
if (moved || !session.isProjectBusy())
|
|
return;
|
|
moved = true;
|
|
if (clears)
|
|
session.clearProject();
|
|
else
|
|
session.setProjectPath(root.filePath(QStringLiteral("Elsewhere.comp")));
|
|
});
|
|
renameFirstLayerOnDisk(path, QStringLiteral("Renamed elsewhere"));
|
|
QTRY_VERIFY_WITH_TIMEOUT(moved, 4000);
|
|
QTRY_VERIFY(!session.isProjectBusy());
|
|
settle();
|
|
QCOMPARE(controller->externalChanges.reloadCount, 0);
|
|
QCOMPARE(session.document().has_value(), !clears);
|
|
if (!clears)
|
|
QCOMPARE(firstName(session), QString("Base"));
|
|
}
|
|
}
|
|
|
|
void ExternalChangeGateTests::aReloadKeepsTheWholeViewAndAnnouncesItLast()
|
|
{
|
|
QTemporaryDir root;
|
|
const QString path = savedProject(root);
|
|
EditorSession session;
|
|
session.installProject(ProjectStore::load(path), path);
|
|
session.viewport.resize(QSizeF(800, 600), 1, session.document().value().size());
|
|
session.zoom(3);
|
|
session.viewport.translate(QSizeF(40, -30));
|
|
const CanvasViewport view = session.viewport;
|
|
const QUuid base = session.document().value().layers[0].id;
|
|
const QUuid blank = session.document().value().layers[1].id;
|
|
session.selectLayer(blank);
|
|
session.extendSelection(base);
|
|
// Disk drops the other selected layer; the active one stays.
|
|
ProjectSnapshot fewer = ProjectStore::load(path);
|
|
fewer.manifest.layers.erase(fewer.manifest.layers.begin() + 1);
|
|
std::optional<std::tuple<CanvasViewport, std::optional<QUuid>, QSet<QUuid>>> last;
|
|
QObject::connect(&session, &EditorSession::changed, [&] { last = {session.viewport, session.activeLayerID(), session.selectedLayerIDs()}; });
|
|
session.reloadProject(fewer);
|
|
QVERIFY(session.viewport == view);
|
|
QCOMPARE(session.activeLayerID().value(), base);
|
|
QCOMPARE(session.selectedLayerIDs(), QSet<QUuid>{base});
|
|
QVERIFY(std::get<0>(last.value()) == view);
|
|
QCOMPARE(std::get<1>(last.value()), std::optional<QUuid>(base));
|
|
QCOMPARE(std::get<2>(last.value()), QSet<QUuid>{base});
|
|
// Both layers on disk: the last signal holds both selected.
|
|
session.reloadProject(ProjectStore::load(path));
|
|
session.selectLayer(blank);
|
|
session.extendSelection(base);
|
|
session.reloadProject(ProjectStore::load(path));
|
|
QCOMPARE(session.selectedLayerIDs(), (QSet<QUuid>{base, blank}));
|
|
QCOMPARE(std::get<2>(last.value()), (QSet<QUuid>{base, blank}));
|
|
}
|
|
|
|
void ExternalChangeGateTests::anUnreadableManifestAsksNothing()
|
|
{
|
|
QTemporaryDir root;
|
|
const QString path = savedProject(root);
|
|
EditorSession session;
|
|
const auto controller = opened(session, path);
|
|
QWidget window;
|
|
window.show();
|
|
controller->window = &window;
|
|
session.renameLayer(session.document().value().layers[0].id, QStringLiteral("Unsaved here"));
|
|
DialogDesk desk;
|
|
// No digest: the package is left alone, no question.
|
|
const QString manifest = path + QStringLiteral("/manifest.json");
|
|
QVERIFY(QFile::remove(manifest));
|
|
QVERIFY(QDir().mkpath(manifest));
|
|
settle();
|
|
QVERIFY(desk.seen.isEmpty());
|
|
QCOMPARE(controller->externalChanges.reloadCount, 0);
|
|
QCOMPARE(firstName(session), QString("Unsaved here"));
|
|
}
|
|
|
|
void ExternalChangeGateTests::saveAsAndOpenMoveTheWatch()
|
|
{
|
|
QTemporaryDir root;
|
|
const QString path = savedProject(root);
|
|
EditorSession session;
|
|
const auto controller = opened(session, path);
|
|
const QString copy = root.filePath(QStringLiteral("Copy.comp"));
|
|
{
|
|
DialogDesk desk;
|
|
desk.replies = {copy};
|
|
QVERIFY(answered([&](auto done) { controller->save(true, done); }));
|
|
}
|
|
QCOMPARE(controller->externalChanges.watcher->path, copy);
|
|
QCOMPARE(controller->externalChanges.knownDigest.value(), ProjectDigest::compute(copy));
|
|
renameFirstLayerOnDisk(copy, QStringLiteral("Copy renamed"));
|
|
QTRY_COMPARE_WITH_TIMEOUT(controller->externalChanges.reloadCount, 1, 4000);
|
|
QCOMPARE(firstName(session), QString("Copy renamed"));
|
|
// The former package is no longer this project.
|
|
renameFirstLayerOnDisk(path, QStringLiteral("Former renamed"));
|
|
settle();
|
|
QCOMPARE(controller->externalChanges.reloadCount, 1);
|
|
// Opening another package moves the watch there.
|
|
QVERIFY(answered([&](auto done) { controller->open(path, done); }));
|
|
QCOMPARE(controller->externalChanges.watcher->path, path);
|
|
QCOMPARE(firstName(session), QString("Former renamed"));
|
|
renameFirstLayerOnDisk(copy, QStringLiteral("Copy again"));
|
|
settle();
|
|
QCOMPARE(controller->externalChanges.reloadCount, 1);
|
|
}
|
|
|
|
void ExternalChangeGateTests::aSecondChangeWhileAskingIsHeard()
|
|
{
|
|
for (const bool revert : {false, true}) {
|
|
QTemporaryDir root;
|
|
const QString path = savedProject(root);
|
|
EditorSession session;
|
|
const auto controller = opened(session, path);
|
|
QWidget window;
|
|
window.show();
|
|
controller->window = &window;
|
|
session.renameLayer(session.document().value().layers[0].id, QStringLiteral("Unsaved here"));
|
|
DialogDesk desk;
|
|
desk.replies = revert ? QStringList{"Revert"} : QStringList{"Keep Mine", "Keep Mine"};
|
|
// A second version arrives while the first question waits.
|
|
desk.note = [&] {
|
|
if (desk.seen.isEmpty()) {
|
|
renameFirstLayerOnDisk(path, QStringLiteral("Second version"));
|
|
QTest::qWait(900);
|
|
}
|
|
return QStringLiteral("pending=%1").arg(controller->externalChanges.pending);
|
|
};
|
|
renameFirstLayerOnDisk(path, QStringLiteral("First version"));
|
|
QTRY_COMPARE_WITH_TIMEOUT(desk.seen.size(), revert ? 1 : 2, 6000);
|
|
QVERIFY2(desk.seen[0].endsWith("|pending=1"), qPrintable(desk.seen[0]));
|
|
QTRY_VERIFY_WITH_TIMEOUT(!controller->externalChanges.checking, 4000);
|
|
settle();
|
|
QCOMPARE(desk.seen.size(), revert ? 1 : 2);
|
|
QVERIFY(!controller->externalChanges.pending);
|
|
QCOMPARE(controller->externalChanges.knownDigest.value(), ProjectDigest::compute(path));
|
|
QCOMPARE(controller->externalChanges.reloadCount, revert ? 1 : 0);
|
|
QCOMPARE(firstName(session), revert ? QString("Second version") : QString("Unsaved here"));
|
|
}
|
|
}
|
|
|
|
QTEST_MAIN(ExternalChangeGateTests)
|
|
#include "ExternalChangeGateTests.moc"
|