Files
OmaPhoto/tests/ProjectWorkspaceSignalTests.cpp
T
雷电芽衣andClaude c864304695
Release / deb (push) Canceled after 0s
Release / rpm (push) Canceled after 0s
Release / arch (push) Canceled after 0s
Release / publish (push) Canceled after 0s
OmaPhoto 1.0.0: the Linux port of Compositor
A layered image editor for Linux and Omarchy, ported from Compositor
(github.com/robbietilton/Compositor), whose Swift source is its
specification. It reads and writes Compositor's .comp projects.

Packages for Ubuntu, Fedora and Arch come from the release workflow;
scripts/install/ holds one install script per distribution family,
and flake.nix serves NixOS.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-26 00:44:52 -04:00

134 lines
5.3 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 "DialogDesk.h"
#include "Document/ProjectWorkspace.h"
#include "IO/ProjectStore.h"
#include <QSemaphore>
#include <QTemporaryDir>
#include <QThreadPool>
#include <QtTest>
// What observers of the workspace see, and what outlives it.
namespace {
// Everything an observer can read from the workspace.
QStringList described(const ProjectWorkspace &workspace)
{
QStringList result{workspace.isManaging() ? "managing" : "idle", workspace.canSwitch() ? "free" : "held"};
for (const std::shared_ptr<ProjectTab> &tab : workspace.tabs())
result << tab->title() + (tab->id == workspace.selectedID() ? "*" : "") + (tab->session.document() ? " drawn" : "");
return result;
}
void store(const QString &path)
{
EditorSession session;
session.createDocument(8, 6);
session.addBlankLayer();
ProjectStore::save(session.projectSnapshot().value(), path);
}
QUrl picture(const QTemporaryDir &folder, const QString &name)
{
QImage image(4, 2, QImage::Format_RGBA8888);
image.fill(Qt::green);
if (!image.save(folder.filePath(name + ".png")))
throw std::runtime_error("the fixture could not be written");
return QUrl::fromLocalFile(folder.filePath(name + ".png"));
}
}
class ProjectWorkspaceSignalTests : public QObject {
Q_OBJECT
private slots:
void theLastSignalOfEveryFlowSeesWhatItLeaves();
void anEmptyDropManagesNothingForLong();
void aWorkspaceThatEndsUnderAQuestionLeavesItHarmless();
};
void ProjectWorkspaceSignalTests::theLastSignalOfEveryFlowSeesWhatItLeaves()
{
QTemporaryDir folder;
store(folder.filePath("Trip.comp"));
ProjectWorkspace workspace;
QStringList seen;
connect(&workspace, &ProjectWorkspace::changed, this, [&] { seen = described(workspace); });
// Runs a flow to its end; names what went stale.
const auto stale = [&](const std::function<void(std::function<void()>)> &flow) {
bool done = false;
seen.clear();
flow([&] { done = true; });
if (!QTest::qWaitFor([&] { return done; }, 10'000))
return QStringLiteral("never finished");
return seen == described(workspace) ? QString() : seen.join("; ") + " != " + described(workspace).join("; ");
};
DialogDesk desk;
QCOMPARE(stale([&](auto done) { workspace.open(folder.filePath("Trip.comp"), [done](bool) { done(); }); }), QString());
QCOMPARE(stale([&](auto done) { workspace.receive({picture(folder, "One")}, std::nullopt, std::nullopt, done); }), QString());
const QUuid trip = workspace.tabs()[0]->id;
const QUuid layer = workspace.tabs()[0]->session.document().value().layers.front().id;
QCOMPARE(stale([&](auto done) { workspace.copyLayer(layer, workspace.selectedID(), std::nullopt, done); }), QString());
desk.replies = {"Don’t Save"};
QCOMPARE(stale([&](auto done) { workspace.close(workspace.selectedID(), done); }), QString());
desk.replies = {"Cancel"};
workspace.tabs()[0]->session.addBlankLayer();
QCOMPARE(stale([&](auto done) { workspace.confirmQuit([done](bool) { done(); }); }), QString());
QCOMPARE(described(workspace), (QStringList{"idle", "free", "Trip* drawn"}));
QCOMPARE(workspace.selectedID(), trip);
}
void ProjectWorkspaceSignalTests::anEmptyDropManagesNothingForLong()
{
ProjectWorkspace workspace;
const QStringList before = described(workspace);
bool done = false;
workspace.receive({}, std::nullopt, std::nullopt, [&] { done = true; });
QVERIFY(!done);
QTRY_VERIFY(done);
QCOMPARE(described(workspace), before);
}
void ProjectWorkspaceSignalTests::aWorkspaceThatEndsUnderAQuestionLeavesItHarmless()
{
auto workspace = std::make_unique<ProjectWorkspace>();
workspace->current().session.createDocument(8, 6);
const std::weak_ptr<ProjectTab> tab = workspace->tabs()[0];
int calls = 0;
workspace->close(workspace->selectedID(), [&] { calls += 1; });
// The question is up; the workspace goes; then the answer.
QTRY_VERIFY(qobject_cast<QMessageBox *>(QApplication::activeModalWidget()) || !QApplication::topLevelWidgets().isEmpty());
workspace.reset();
QVERIFY(!tab.expired());
DialogDesk desk;
desk.replies = {"Don’t Save"};
QTRY_COMPARE(desk.seen.size(), 1);
// The answer frees the tab it held; nobody is called.
QTRY_VERIFY(tab.expired());
QCOMPARE(calls, 0);
// A copy in flight ends with its workspace as well.
QThreadPool *pool = QThreadPool::globalInstance();
const int workers = pool->maxThreadCount();
QSemaphore held, gate;
const auto letGo = qScopeGuard([&] {
gate.release(workers);
pool->waitForDone();
});
for (int index = 0; index < workers; ++index) {
pool->start([&] {
held.release();
gate.acquire();
});
}
QVERIFY(held.tryAcquire(workers, 10'000));
auto copying = std::make_unique<ProjectWorkspace>();
copying->current().session.createDocument(8, 6);
copying->current().session.addBlankLayer();
copying->copyLayer(copying->current().session.activeLayerID().value(), std::nullopt, std::nullopt, [&] { calls += 1; });
QVERIFY(copying->isManaging());
copying.reset();
gate.release(workers);
QVERIFY(pool->waitForDone(10'000));
QTest::qWait(50);
QCOMPARE(calls, 0);
}
QTEST_MAIN(ProjectWorkspaceSignalTests)
#include "ProjectWorkspaceSignalTests.moc"