mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
[Spoilers] Preserve spoiler files through update and reload failures (#7707)
* Preserve spoiler files and serialize update reloads * Surface spoiler removal reload failures and allow disabled-state retry * Address review feedback on spoiler update lifecycle Only remove the spoiler file when the user unchecks the setting; opening Settings no longer queues an unrequested reload for a leftover file. Remove the spoilersUpdatedSuccessfully signal, which had no receiver. * Fix spoiler settings state after reopen and while disabled Reopen Settings while an application-owned update is running: the new page now observes that updater, so the button reads Updating... and re-enables on completion instead of offering a second update that cancels the first. Observation does not take ownership: only updaters the page creates are retired with deleteLater, because the launch check is stack-allocated. When spoilers are off, the hint now describes what Reload Card Database actually does, stays readable, and wraps instead of clipping. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
3a656c9c4a
commit
0de92c06f8
9 changed files with 648 additions and 229 deletions
|
|
@ -190,6 +190,15 @@ cockatrice_add_test(
|
|||
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||
${CMAKE_SOURCE_DIR}/libcockatrice_network
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
spoiler_file_test
|
||||
SOURCES
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/client/network/update/card_spoiler/spoiler_file.cpp
|
||||
spoiler_file_test.cpp
|
||||
INCLUDE_DIRS
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||
)
|
||||
# The installer's holder probe is Windows-only, but the wording it produces and the
|
||||
# question which programs are the payload's own are plain C++ and are what a stuck user
|
||||
# has to act on, so they are tested everywhere.
|
||||
|
|
|
|||
149
tests/spoiler_file_test.cpp
Normal file
149
tests/spoiler_file_test.cpp
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
#include "client/network/update/card_spoiler/spoiler_file.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QTemporaryDir>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
#include <csignal>
|
||||
#include <sys/resource.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
class SpoilerFileTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
QTemporaryDir directory;
|
||||
QString path;
|
||||
QString error;
|
||||
const QByteArray original = "<cockatrice_carddatabase version=\"4\"><cards/></cockatrice_carddatabase>";
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
ASSERT_TRUE(directory.isValid());
|
||||
path = directory.filePath("spoiler.xml");
|
||||
QFile file(path);
|
||||
ASSERT_TRUE(file.open(QIODevice::WriteOnly));
|
||||
ASSERT_EQ(file.write(original), original.size());
|
||||
}
|
||||
|
||||
QByteArray read()
|
||||
{
|
||||
QFile file(path);
|
||||
EXPECT_TRUE(file.open(QIODevice::ReadOnly));
|
||||
return file.readAll();
|
||||
}
|
||||
|
||||
SpoilerFile::SaveResult save(const QByteArray &data)
|
||||
{
|
||||
return SpoilerFile::save(path, data, [] { return true; }, error);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(SpoilerFileTest, InvalidDocumentsPreserveExistingFile)
|
||||
{
|
||||
for (const QByteArray &invalid :
|
||||
{QByteArray(), QByteArray("<html><cards/></html>"),
|
||||
QByteArray("<cockatrice_carddatabase version=\"2\"><cards/></cockatrice_carddatabase>"),
|
||||
QByteArray("<cockatrice_carddatabase><cards/></cockatrice_carddatabase>"),
|
||||
QByteArray("<cockatrice_carddatabase version=\"4\"><sets/></cockatrice_carddatabase>"),
|
||||
QByteArray("<cockatrice_carddatabase version=\"4\"><cards>"),
|
||||
QByteArray("<cockatrice_carddatabase version=\"4\"><cards/></cockatrice_carddatabase><extra/>")}) {
|
||||
EXPECT_EQ(save(invalid), SpoilerFile::SaveResult::Failed);
|
||||
EXPECT_FALSE(error.isEmpty());
|
||||
EXPECT_EQ(read(), original);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(SpoilerFileTest, SupportedVersionsAreAccepted)
|
||||
{
|
||||
EXPECT_TRUE(SpoilerFile::validate(original).isEmpty());
|
||||
EXPECT_TRUE(
|
||||
SpoilerFile::validate("<cockatrice_carddatabase version=\"3\"><cards/></cockatrice_carddatabase>").isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(SpoilerFileTest, IdenticalFileIsUnchanged)
|
||||
{
|
||||
EXPECT_EQ(save(original), SpoilerFile::SaveResult::Unchanged);
|
||||
EXPECT_TRUE(error.isEmpty());
|
||||
EXPECT_EQ(read(), original);
|
||||
}
|
||||
|
||||
TEST_F(SpoilerFileTest, CompleteFileHashDetectsChangesAfterLargeCommonPrefix)
|
||||
{
|
||||
QByteArray changed = "<cockatrice_carddatabase version=\"4\"><!--";
|
||||
changed += QByteArray(100000, 'x');
|
||||
changed += "--><cards/></cockatrice_carddatabase>";
|
||||
ASSERT_EQ(save(changed), SpoilerFile::SaveResult::Written);
|
||||
changed.replace("<cards/>", "<cards></cards>");
|
||||
EXPECT_EQ(save(changed), SpoilerFile::SaveResult::Written);
|
||||
EXPECT_EQ(read(), changed);
|
||||
EXPECT_TRUE(error.isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(SpoilerFileTest, CancellationBeforeWritePreservesExistingFile)
|
||||
{
|
||||
EXPECT_EQ(SpoilerFile::save(path, original, [] { return false; }, error), SpoilerFile::SaveResult::Failed);
|
||||
EXPECT_FALSE(error.isEmpty());
|
||||
EXPECT_EQ(read(), original);
|
||||
}
|
||||
|
||||
TEST_F(SpoilerFileTest, CancellationBeforeCommitPreservesExistingFile)
|
||||
{
|
||||
int checks = 0;
|
||||
EXPECT_EQ(SpoilerFile::save(
|
||||
path, original + "\n", [&checks] { return ++checks == 1; }, error),
|
||||
SpoilerFile::SaveResult::Failed);
|
||||
EXPECT_EQ(checks, 2);
|
||||
EXPECT_FALSE(error.isEmpty());
|
||||
EXPECT_EQ(read(), original);
|
||||
EXPECT_EQ(QDir(directory.path()).entryList(QDir::Files), QStringList{"spoiler.xml"});
|
||||
}
|
||||
|
||||
TEST_F(SpoilerFileTest, MissingParentIsCreated)
|
||||
{
|
||||
path = directory.filePath("nested/spoiler.xml");
|
||||
EXPECT_EQ(save(original), SpoilerFile::SaveResult::Written);
|
||||
EXPECT_EQ(read(), original);
|
||||
}
|
||||
|
||||
TEST_F(SpoilerFileTest, FileInParentPathReportsFailureWithoutModifyingIt)
|
||||
{
|
||||
EXPECT_EQ(SpoilerFile::save(
|
||||
QDir(path).filePath("spoiler.xml"), original, [] { return true; }, error),
|
||||
SpoilerFile::SaveResult::Failed);
|
||||
EXPECT_FALSE(error.isEmpty());
|
||||
EXPECT_EQ(read(), original);
|
||||
}
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
TEST_F(SpoilerFileTest, ShortWriteLeavesExistingFileIntact)
|
||||
{
|
||||
// Limit only the child process: a real short write exercises QSaveFile's
|
||||
// buffered flush failure without depending on disk capacity or file ownership.
|
||||
const QByteArray changed = "<cockatrice_carddatabase version=\"4\"><!--" + QByteArray(100000, 'x') +
|
||||
"--><cards/></cockatrice_carddatabase>";
|
||||
EXPECT_EXIT(
|
||||
{
|
||||
struct rlimit limit;
|
||||
if (getrlimit(RLIMIT_FSIZE, &limit) != 0) {
|
||||
_exit(2);
|
||||
}
|
||||
limit.rlim_cur = 128;
|
||||
if (setrlimit(RLIMIT_FSIZE, &limit) != 0) {
|
||||
_exit(3);
|
||||
}
|
||||
std::signal(SIGXFSZ, SIG_IGN);
|
||||
const auto result = save(changed);
|
||||
_exit(result == SpoilerFile::SaveResult::Failed && !error.isEmpty() ? 0 : 1);
|
||||
},
|
||||
::testing::ExitedWithCode(0), "");
|
||||
EXPECT_EQ(read(), original);
|
||||
EXPECT_EQ(QDir(directory.path()).entryList(QDir::Files), QStringList{"spoiler.xml"});
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
Loading…
Add table
Add a link
Reference in a new issue